mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
feat(mcp): catalogue préconfiguré + état persisté + infra pytest
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fd6edf503
commit
76677637a1
4 files changed
+131
No files matched your search
@@ -0,0 +1,89 @@
|
||||
"""Client MCP : catalogue préconfiguré + sessions vers les serveurs activés.
|
||||
|
||||
Un serveur désactivé n'est jamais démarré et n'expose aucun outil au modèle
|
||||
(chaque outil injecté coûte du contexte). Connexion lazy au premier message,
|
||||
session réutilisée ensuite. Toute panne est non bloquante pour le chat.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from . import db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MCP_KEY = "mcp"
|
||||
|
||||
# Catalogue embarqué. command=None => serveur "custom" (commande utilisateur).
|
||||
CATALOG: dict[str, dict] = {
|
||||
"playwright": {
|
||||
"label": "Playwright (navigateur)",
|
||||
"description": "Pilote un vrai navigateur : naviguer, cliquer, lire "
|
||||
"la console, captures. L'agent teste réellement ses pages.",
|
||||
"command": ["npx", "@playwright/mcp@latest", "--headless"],
|
||||
"url_param": False,
|
||||
"env_params": [],
|
||||
# Limite le nombre d'outils injectés dans le prompt.
|
||||
"expose": [
|
||||
"browser_navigate", "browser_click", "browser_type",
|
||||
"browser_snapshot", "browser_console_messages",
|
||||
"browser_take_screenshot",
|
||||
],
|
||||
},
|
||||
"context7": {
|
||||
"label": "Context7 (documentation)",
|
||||
"description": "Documentation à jour de n'importe quelle librairie ou "
|
||||
"framework (React, FastAPI, Tailwind…).",
|
||||
"command": ["npx", "-y", "@upstash/context7-mcp"],
|
||||
"url_param": False,
|
||||
"env_params": [],
|
||||
"expose": None,
|
||||
},
|
||||
"fetch": {
|
||||
"label": "Fetch (lecture web)",
|
||||
"description": "Lit proprement n'importe quelle URL (markdown épuré).",
|
||||
"command": ["python", "-m", "mcp_server_fetch"],
|
||||
"url_param": False,
|
||||
"env_params": [],
|
||||
"expose": None,
|
||||
},
|
||||
"searxng": {
|
||||
"label": "SearxNG (recherche web)",
|
||||
"description": "Vraie recherche web via une instance SearxNG "
|
||||
"(renseigner SEARXNG_URL).",
|
||||
"command": ["npx", "-y", "mcp-searxng"],
|
||||
"url_param": False,
|
||||
"env_params": ["SEARXNG_URL"],
|
||||
"expose": None,
|
||||
},
|
||||
"custom": {
|
||||
"label": "Personnalisé",
|
||||
"description": "N'importe quel serveur MCP : colle une commande "
|
||||
"(stdio) ou une URL (streamable HTTP).",
|
||||
"command": None,
|
||||
"url_param": True,
|
||||
"env_params": [],
|
||||
"expose": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_mcp_state() -> dict:
|
||||
"""État activé/params de chaque serveur du catalogue (défaut : désactivé)."""
|
||||
stored = db.get_config_value(MCP_KEY) or {}
|
||||
return {
|
||||
sid: {
|
||||
"enabled": bool(stored.get(sid, {}).get("enabled", False)),
|
||||
"params": dict(stored.get(sid, {}).get("params", {})),
|
||||
}
|
||||
for sid in CATALOG
|
||||
}
|
||||
|
||||
|
||||
def set_mcp_state(sid: str, *, enabled: bool, params: dict) -> dict:
|
||||
if sid not in CATALOG:
|
||||
raise KeyError(sid)
|
||||
state = get_mcp_state()
|
||||
state[sid] = {"enabled": enabled, "params": dict(params)}
|
||||
db.set_config_value(MCP_KEY, state)
|
||||
return state
|
||||
@@ -8,3 +8,6 @@ httpx==0.28.1
|
||||
pydantic==2.12.5
|
||||
pydantic-settings==2.7.1
|
||||
psutil==7.2.2
|
||||
mcp>=1.9
|
||||
pytest>=8.3
|
||||
pytest-asyncio>=0.25
|
||||
Whitespace-only changes.
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("DATA_DIR", tempfile.mkdtemp())
|
||||
os.environ.setdefault("WORKSPACE_DIR", tempfile.mkdtemp())
|
||||
|
||||
from app import db # noqa: E402
|
||||
from app import mcp_client # noqa: E402
|
||||
|
||||
db.init_db()
|
||||
|
||||
|
||||
def test_catalog_has_preconfigured_servers():
|
||||
for sid in ("playwright", "context7", "fetch", "searxng", "custom"):
|
||||
assert sid in mcp_client.CATALOG
|
||||
assert mcp_client.CATALOG[sid]["label"]
|
||||
|
||||
|
||||
def test_state_defaults_disabled():
|
||||
state = mcp_client.get_mcp_state()
|
||||
assert set(state) == set(mcp_client.CATALOG)
|
||||
assert all(not s["enabled"] for s in state.values())
|
||||
|
||||
|
||||
def test_toggle_persists():
|
||||
mcp_client.set_mcp_state("fetch", enabled=True, params={})
|
||||
assert mcp_client.get_mcp_state()["fetch"]["enabled"] is True
|
||||
mcp_client.set_mcp_state("fetch", enabled=False, params={})
|
||||
assert mcp_client.get_mcp_state()["fetch"]["enabled"] is False
|
||||
|
||||
|
||||
def test_custom_params_persist():
|
||||
mcp_client.set_mcp_state(
|
||||
"custom", enabled=False, params={"command": "npx -y some-mcp"}
|
||||
)
|
||||
assert (
|
||||
mcp_client.get_mcp_state()["custom"]["params"]["command"]
|
||||
== "npx -y some-mcp"
|
||||
)
|
||||
Reference in new issue
Block a user