feat(fusion): serveur MCP complété (ACC-5.1) + pont MCP ObsiGate (ACC-2.2) — 7.83.0
FlowDeck CI / test (push) Failing after 3h11m23s
FlowDeck CI / lint (push) Failing after 5s
FlowDeck CI / docker (push) Skipped

ACC-5.1 (#24) — app/routers/mcp_server.py : catalogue §7.2 complété
(list_projects, get_project, read_page tronqué proprement,
list_project_tasks), search_pages en plein texte, liens profonds +
kind sur chaque résultat, scopes du jeton appliqués (écriture
refusée -32002 sans scope write) et journalisation §7.4 de chaque
tools/call dans api_audit_log (clés d'arguments seulement).

ACC-2.2 (#20) — pont MCP Flowdeck → ObsiGate, côté client :
- mcp_client : serveurs à session (capture/réémission de
  Mcp-Session-Id, handshake avant tools/call), décodage SSE
  multi-messages par id de réponse, classification des outils à la
  découverte (readOnlyHint sinon heuristique ; lecture par défaut).
- preset connecteur « obsigate » (URL dérivée dev/prod), kinds MCP
  factorisés (connectors.MCP_KINDS), option dans le panneau agent.
- garde-fou n° 2 : permission_manager.is_write_tool couvre les
  outils MCP écrivants → portes politique d'approbation
  (agent_engine) et rôles (assert_can) ; McpTool bloque tout
  apply_* distant (patron §7.5).

Tests : test_fusion_mcp_server 18, nouveau test_fusion_pont_obsigate
7 (faux serveur ObsiGate à session stricte via le vrai _rpc).
Preuve réelle T0-T5 : attend la clé ObsiGate dédiée (1 vault) et
Q-A/Q-B côté Bruno.
This commit is contained in:
Spark
2026-10-26 12:38:04 -04:00
parent fd79a49d63
commit 19b27698f2
13 changed files with 1044 additions and 51 deletions
+17 -3
View File
@@ -24,7 +24,11 @@ from app.services.sso_provisioning import decrypt_secret, encrypt_secret
logger = logging.getLogger(__name__)
NATIVE_KINDS = ("gitea", "github", "web", "google", "ms365")
CUSTOM_KINDS = ("custom", "discord", "telegram", "mcp", "tiktak")
CUSTOM_KINDS = ("custom", "discord", "telegram", "mcp", "tiktak", "obsigate")
#: Kinds personnels qui parlent MCP (probe = handshake + tools/list,
#: outils dynamiques ``mcp_<serveur>_*``) — générique + les deux presets
#: de fédération du programme (TikTak ACC-2.1, ObsiGate ACC-2.2).
MCP_KINDS = ("mcp", "tiktak", "obsigate")
AUTH_SCHEMES = ("bearer", "bot", "none")
# Presets proposés par le formulaire du menu + (le jeton reste toujours saisi
# par l'utilisateur — jamais codé en dur ici).
@@ -45,6 +49,16 @@ def _presets() -> dict:
# v7.82.0 : URL dérivée de la config (TIKTAK_URL ; défaut officiel).
"tiktak": {"url": f"{draco_urls.tiktak_url()}/mcp", "auth": "bearer",
"hint": "TikTak → Réglages → Intégrations → clé API tck_…"},
# Fusion ACC-2.2 — pont MCP vers ObsiGate (plan « étape 1 :
# lectures seules », tests T0–T5) : serveur MCP d'ObsiGate sur
# /mcp (Streamable HTTP à session — le client gère le
# Mcp-Session-Id). La clé se crée dans ObsiGate →
# Configurations → Clés API & MCP, dédiée au pont et limitée
# à UN SEUL vault au départ : c'est le jeton qui fait la
# portée (garde-fou n° 1 du plan), pas ce preset.
"obsigate": {"url": f"{draco_urls.obsigate_url()}/mcp", "auth": "bearer",
"hint": "ObsiGate → Configurations → Clés API & MCP → "
"clé dédiée au pont, limitée à un seul vault"},
}
@@ -293,7 +307,7 @@ async def probe(connector: str, user_id: int | None = None) -> dict:
"""Teste un connecteur et **persiste** le résultat (personnel uniquement)."""
kind, ref = _resolve(connector)
try:
if kind in ("mcp", "tiktak"):
if kind in MCP_KINDS:
from app.services import mcp_client
tools = await mcp_client.initialize_and_list(int(ref))
status, detail = "ok", f"MCP · {len(tools)} outil(s)"
@@ -343,7 +357,7 @@ async def connector_fetch(connector: str, path: str = "", query: str = "",
path = (path or "").strip()
if kind in OAUTH_KINDS:
return await oauth_connectors.api_get(kind, user_id, path)
if kind in ("mcp", "tiktak"):
if kind in MCP_KINDS:
from app.services import mcp_client
return {"status": "ok", "text": mcp_client.tools_text(int(ref))}
if kind == "gitea":