mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +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==2.12.5
|
||||||
pydantic-settings==2.7.1
|
pydantic-settings==2.7.1
|
||||||
psutil==7.2.2
|
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