02-ce-que-je-construis/specs/ia-mcp.md

Lot 1 — Serveur MCP (tlr-mcp) : spécification

Spécification consolidée du serveur MCP : exposer la documentation comme outils à un client agentique, via le standard MCP officiel, avec une couche de gouvernance multi-tenant. Référence unique : ce document remplace et réconcilie les specs MCP éparses (voir §13). Rattaché au cadre : specs/ia-vitrine.md. Statut : spec implémentable (V1 figée).


1. Objectif et positionnement

tlr-mcp est un serveur MCP multi-tenant qui rend la documentation actionnable par un agent IA (Claude Desktop, Cursor…). C'est le différenciateur de la vitrine : prouver la maîtrise du standard d'interopérabilité agentique, pas seulement l'appel d'un LLM.

  • Umbrella tlr-mcp (produit transverse, mcp.telaria.dev) ; Codexia est un tenant/consommateur.
  • Forme : bundle Symfony (cf. cadre §3).
  • V1 lecture seule, adossé au cÅ“ur RAG (Lot 0) pour la recherche.

2. Décision d'architecture : MCP officiel + couche de gouvernance

Deux familles de specs préexistaient (voir §13). La consolidation tranche :

  • Protocole = MCP officiel (révision 2025-11-25) : JSON-RPC 2.0, lifecycle, transports stdio et Streamable HTTP, autorisation OAuth 2.1. (Repris de la « famille A ».)
  • Gouvernance = couche Telaria par-dessus : multi-tenant, scopes, quotas, ADN (isolation projet, inputs/legacy/ en lecture seule), audit, sandbox. (Repris de la « famille B », branché sur l'auth officielle.)
  • On abandonne le protocole HTTP maison (enveloppe status/data/errors, codes MCP_ERR_*) : non conforme au fil MCP. Les erreurs passent par l'objet error JSON-RPC, avec un code applicatif stable dans error.data.code.

Rationale : un serveur qui ne parle pas le vrai protocole MCP ne « démontre » pas MCP. La conformité est le cœur de la valeur vitrine.


3. Protocole (MCP officiel)

  • JSON-RPC 2.0 pour tous les échanges (jsonrpc: "2.0", UTF-8 ; une notification n'a pas d'id ; erreurs via l'objet error).
  • Cycle de vie : initialize → notifications/initialized → appels métier ; capacités négociées à l'initialisation.
  • Versionnement : format YYYY-MM-DD, cible 2025-11-25 ; en HTTP, en-tête MCP-Protocol-Version (version invalide → 400).
  • Transports :
    • stdio (messages ligne par ligne sur stdin/stdout ; stderr = logs) — idéal pour les clients desktop.
    • Streamable HTTP (un endpoint POST/GET ; Accept: application/json, text/event-stream ; réponse JSON unique ou flux SSE) — pour l'usage distant.
  • Sécurité transport : validation de l'en-tête Origin (sinon 403), liaison localhost en local, authentification obligatoire, gestion de session via MCP-Session-Id.

Références : MCP 2025-11-25 (lifecycle, transports, tools, authorization) ; JSON-RPC 2.0.


4. Capacités exposées

Capacité MCP V1 Plus tard
Tools (outils invocables) ✅ (lecture seule) Édition, audit qualité
Resources (contenus par URI) — Exposer les docs en ressources
Prompts (gabarits) — Modèles de prompts par projet
Utilities (progress, logging…) logging de base progress/cancellation/tasks

5. Périmètre V1 (lecture seule, adossé au cœur)

Trois outils, alignés sur le besoin « interroger la doc » :

  • list_docs : liste les documents indexés (chemin, titre), filtrés (exclut SCRATCH.md et inputs/legacy/).
  • read_doc : retourne le contenu d'un document autorisé + ses métadonnées.
  • search_docs : recherche sémantique via le RetrievalService du cÅ“ur (L0) → top-k passages avec score et source (chemin, section, ancre).

Changement assumé : l'ancien ancrage V1 draft_readme_basic (génération) ne sollicite ni la doc ni le cœur ; il est déclassé. L'ancre V1 devient search_docs, qui exerce toute la chaîne RAG de bout en bout. Les outils d'édition/qualité (validate_markdown, check_links, apply_patch, audit_rgaa, build_toc…) passent en V2+.


6. Gouvernance et sécurité (couche Telaria)

  • Multi-tenant : un tenant isole projets, tokens, quotas, audits. Un ApiClient (clé) porte scopes, quotas et projets accessibles. Aucune action inter-tenant.
  • Tokens : seul le hash est stocké ; rotation et révocation immédiate possibles ; expiration configurable. Format aligné OAuth 2.1.
  • Scopes : scopes outils (autorisent un outil) + scopes projets (restreignent les projets). Vérifiés avant toute exécution.
  • ADN (règles absolues) : l'IA ne lit/agit jamais hors de la racine du projet cible ; inputs/legacy/ est lecture seule ; SCRATCH.md n'est jamais exposé (cohérent avec .aiignore et le cÅ“ur L0).
  • Quotas / rate limiting par (tenant, outil) uniquement en V1 — pas de quota global serveur (axe unique pour rester lisible en démo) ; dépassement → erreur explicite.
  • Audit : chaque appel (succès/refus) tracé par tenant, projet, client, outil.
  • Sandbox : exécution bornée (CPU/mémoire/FS) par tenant.

7. Modèle de données (Symfony)

  • Tenant : id, name, status.
  • ApiClient : id, tenant_id, token_hash, scopes, rate_limit.
  • Project : id, tenant_id, root_path (ou index logique), status.
  • ToolAuditLog : id, tenant_id, project_id, tool_name, status, timestamp, error_code.

La recherche réutilise l'index du cœur (L0) ; le serveur MCP n'a pas son propre stockage de contenu.


8. Implémentation Symfony (bundle tlr-mcp)

  • ToolInterface : getName(): string, getSchema(): array, execute(array $args, McpContext $ctx): ToolResult.
  • ToolRegistry : découverte par tag de service mcp.tool.
  • Méthodes JSON-RPC standard MCP (révision 2025-11-25, obligatoires pour qu'un client comme Claude Desktop ou Cursor puisse invoquer les outils) :
    • tools/list : renvoie la liste des outils, avec leur name, description, inputSchema. Auth optionnelle (conforme au standard MCP : un client liste les capacités avant de présenter son token) :
      • sans token → catalogue statique complet (les types d'outils V1, sans aucune donnée tenant/projet) ;
      • avec token → catalogue filtré par scopes (tool:<name>) du client courant.
      • Rationale : la découverte n'expose pas de donnée sensible ; la frontière de sécurité est portée par tools/call, pas par tools/list. (Acté 2026-05-29 — arbitrage Doc Lead suite Q1 codexia #23 ; lève l'ambiguïté « filtré par scopes » qui laissait croire à une auth obligatoire en amont.)
    • tools/call : invoque un outil nommé avec ses arguments ; route vers la méthode execute correspondante du ToolRegistry après pipeline. Auth toujours obligatoire (token + scope tool:<name> ∧ project:<slug> + quota par tenant+outil).
    • Toute autre méthode JSON-RPC = erreur METHOD_NOT_FOUND (-32601).
  • Pipeline d'exécution : (1) parse/valide JSON-RPC → (2) résout tenant/projet → (3) vérifie scopes → (4) applique l'ADN (racine, inputs/legacy/, SCRATCH.md) → (5) valide l'inputSchema → (6) exécute → (7) audit + métriques.
  • search_docs délègue au RetrievalService du cÅ“ur (L0).
  • Contrôleurs de transport : un pour stdio, un pour Streamable HTTP.

9. Schémas JSON des outils V1

Conventions : additionalProperties: false, enums pour les choix fermés, tailles max documentées, schémas de sortie versionnés.

// search_docs — entrée
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": { "type": "string", "description": "Question en langage naturel." },
    "k": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 }
  },
  "required": ["query"],
  "additionalProperties": false
}
// search_docs — sortie
{
  "type": "object",
  "properties": {
    "status": { "type": "string", "enum": ["ok", "warning", "error"] },
    "hits": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "path": { "type": "string" },
          "section": { "type": "string" },
          "score": { "type": "number" },
          "excerpt": { "type": "string" }
        },
        "required": ["path", "score", "excerpt"],
        "additionalProperties": false
      }
    }
  },
  "required": ["status", "hits"],
  "additionalProperties": false
}

list_docs

// list_docs — entrée
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "prefix": { "type": "string", "description": "Filtre optionnel sur un préfixe de chemin (ex. 'specs/')." },
    "limit": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 }
  },
  "additionalProperties": false
}
// list_docs — sortie
{
  "type": "object",
  "properties": {
    "status": { "type": "string", "enum": ["ok", "warning", "error"] },
    "documents": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "path": { "type": "string" },
          "title": { "type": "string" }
        },
        "required": ["path", "title"],
        "additionalProperties": false
      }
    },
    "truncated": { "type": "boolean", "description": "true si le résultat a été tronqué à `limit`." }
  },
  "required": ["status", "documents", "truncated"],
  "additionalProperties": false
}

read_doc — renvoie le Markdown brut (cf. §14.4) + métadonnées :

// read_doc — entrée
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "path": { "type": "string", "description": "Chemin relatif à la racine du projet (ex. 'specs/ia-coeur.md')." }
  },
  "required": ["path"],
  "additionalProperties": false
}
// read_doc — sortie
{
  "type": "object",
  "properties": {
    "status": { "type": "string", "enum": ["ok", "warning", "error"] },
    "path": { "type": "string" },
    "title": { "type": "string" },
    "content": { "type": "string", "description": "Markdown brut (UTF-8), tel que stocké. Aucune normalisation côté serveur." },
    "metadata": {
      "type": "object",
      "properties": {
        "mtime": { "type": "string", "format": "date-time" },
        "content_hash": { "type": "string", "description": "Hash du contenu (cohérent avec celui du cœur RAG)." },
        "size_bytes": { "type": "integer", "minimum": 0 }
      },
      "required": ["mtime", "content_hash", "size_bytes"],
      "additionalProperties": false
    }
  },
  "required": ["status", "path", "title", "content", "metadata"],
  "additionalProperties": false
}

Rationale read_doc = Markdown brut : un agent LLM consomme directement le Markdown ; toute normalisation côté serveur (rendu HTML, désanchrage…) détruit de l'information utile (ancres, blocs de code, structure). Cf. §14.4.


10. Tests unitaires (cas concrets)

Protocole

  • Requête sans jsonrpc: "2.0" → erreur JSON-RPC -32600 (Invalid Request).
  • Notification contenant un id → rejet.
  • Appel métier avant initialize → erreur de cycle de vie.
  • En-tête MCP-Protocol-Version invalide (HTTP) → 400.
  • Origin non autorisé (Streamable HTTP) → 403.

Authentification & scopes

  • Requête sans token → erreur (error.data.code = "AUTH_REQUIRED").
  • Token révoqué → refus.
  • Scope outil manquant pour search_docs → refus (FORBIDDEN_TOOL).
  • Scope projet manquant → refus (FORBIDDEN_PROJECT).

ADN / gouvernance

  • read_doc d'un chemin hors racine projet → refus (INVALID_PATH).
  • list_docs / search_docs n'incluent jamais SCRATCH.md ni inputs/legacy/.
  • Toute écriture ciblant inputs/legacy/ (V2) → refus (LEGACY_READONLY).

Multi-tenant

  • ApiClient du tenant A demandant un projet du tenant B → refus + entrée d'audit.

Outils V1

  • inputSchema : search_docs sans query → erreur de validation (VALIDATION).
  • search_docs : avec RetrievalService mocké, renvoie k hits triés par score, avec path/excerpt.
  • read_doc : document autorisé → contenu + métadonnées ; document exclu → refus.

Quotas & audit

  • Dépassement de quota → erreur explicite (RATE_LIMIT).
  • Chaque appel (succès et refus) produit une entrée ToolAuditLog.

10.bis Catalogue d'erreurs

Deux niveaux : codes JSON-RPC standard (couche protocole) et code applicatif stable dans error.data.code (couche métier). Cohérent avec §2.

Couche error.code JSON-RPC error.data.code (applicatif) Quand ?
Protocole -32700 — Parse error (JSON invalide).
Protocole -32600 — Invalid Request (manque jsonrpc: "2.0", notification avec id, ou appel métier reçu avant initialize — état de protocole invalide).
Protocole -32601 — Method Not Found (outil inconnu).
Protocole -32602 VALIDATION Invalid Params (échec validation inputSchema).
Protocole -32603 INTERNAL Internal error (exception inattendue côté serveur).
Auth -32000 AUTH_REQUIRED Token absent ou invalide.
Auth -32000 TOKEN_REVOKED Token révoqué ou expiré.
Gouvernance -32000 FORBIDDEN_TOOL Scope outil manquant.
Gouvernance -32000 FORBIDDEN_PROJECT Scope projet manquant ou cross-tenant.
ADN -32000 INVALID_PATH Chemin hors racine projet ; SCRATCH.md ; chemin non normalisé.
ADN -32000 LEGACY_READONLY Tentative d'écriture dans inputs/legacy/ (V2+).
Quotas -32000 RATE_LIMIT Quota tenant/outil dépassé.

Convention : error.data.code est stable (contrat client). Le error.message est libre (UX) et peut évoluer. -32000 est la plage « Server error » réservée par JSON-RPC pour les erreurs applicatives.

Cycle de vie (LIFECYCLE) : un appel métier reçu avant initialize est traité comme -32600 Invalid Request (cf. ligne Protocole ci-dessus), sans code applicatif dédié. Décision : ne pas introduire de code LIFECYCLE au catalogue — Invalid Request couvre sémantiquement « état du protocole invalide » et on évite d'élargir le contrat. (Acté 2026-05-29 — arbitrage Doc Lead suite Q2 codexia #23.)


11. Démo / client

  • Démo V1 = Claude Desktop (transport stdio, cf. §14.1) : déclaration du serveur MCP dans claude_desktop_config.json, démonstration de search_docs / read_doc / list_docs sur la doc Codexia. Tuto déjà produit : tutos/ia/brancher-mcp-claude-desktop-cursor.md.
  • Cursor = cible secondaire (même configuration MCP côté client, validation que le bundle fonctionne hors Claude).
  • Capture d'écran de la démo pour la vitrine (pas d'accès public requis).
  • Streamable HTTP = V1.1 (cf. §14.1), avec son propre tuto de déploiement quand il arrivera.

12. Production documentaire d'accompagnement (doctrine)

Concept introduit Forme Emplacement Statut
MCP : c'est quoi, à quoi ça sert (concepts) Fiche agents/…/4-6 ✅ produit
Serveur MCP minimal en Symfony (tool + transport) Tuto tutos/ia/serveur-mcp-symfony.md ✅ produit
Brancher un serveur MCP sur Claude Desktop / Cursor Tuto tutos/ia/brancher-mcp-claude-desktop-cursor.md ✅ produit

13. Ce que cette spec consolide

Cette spec est la référence unique du serveur MCP. Le détail technique reste dans bundles/tlr-mcp.md + bundles/tlr-mcp/* (protocole officiel, catalogue d'outils, schémas, implémentation Symfony). Les anciennes specs source codexia-doc-ia.md (gouvernance) et telaria-mcp-tenants.md (multi-tenant) ont été absorbées ici puis supprimées (consolidation réalisée).


14. Décisions V1 (tranchées 2026-05-28)

Anciens points ouverts, désormais figés pour rendre la spec implémentable. Les décisions sont réversibles si l'usage réel le justifie (V1.1 / V2).

14.1 Transport — stdio en V1, Streamable HTTP en V1.1

  • V1 = stdio seul. Démo vitrine = Claude Desktop ; stdio = zéro infrastructure réseau, auth implicite (process local lancé par le client MCP). Premier contact propre avec le protocole sans les complications HTTP.
  • V1.1 = ajouter Streamable HTTP : OAuth 2.1 complet, TLS, gestion MCP-Session-Id, validation Origin, déploiement sur mcp.telaria.dev. Découpé en lot séparé pour ne pas mélanger les coûts.
  • Aucun code transport-spécifique ne fuite dans les outils — l'McpContext reste indépendant du transport.

14.2 Format de token — opaque (32 bytes random base64url), hash en DB

  • V1 = token opaque. En stdio local, on n'a pas besoin de validation distribuée (pas de service tiers à éviter, pas de round-trip à économiser). Opaque + hash en DB = rotation et révocation immédiates (déjà §6), simplicité maximale.
  • Pas de JWT en V1 : injustifié sans HTTP distribué. Si V1.1 (HTTP) amène un besoin de validation sans round-trip DB, on basculera côté émetteur OAuth 2.1 (JWT signés) à ce moment-là — décision encapsulée dans le service d'auth, transparente pour les outils.
  • Transmission du token au transport stdio : variable d'environnement MCP_TOKEN lue au démarrage du process bin/console mcp:serve (transport stdio ; le nom mcp:stdio n'existe pas — confirmé Lead dev 2026-06-01) par le client MCP (Claude Desktop, Cursor). Pas de header HTTP en stdio, pas d'argument CLI (laisserait le token dans ps/historique shell). Pour HTTP V1.1, header Authorization: Bearer <token> standard. (Acté 2026-05-29 — rétro-doc suite implémentation V1 par tlr-mcp.)

14.3 Nommage des scopes — tool:<name> ∧ project:<slug> (AND, deux dimensions)

  • Convention figée : un ApiClient porte une liste de scopes ; l'autorisation = toutes les dimensions requises présentes.
  • Scopes outils : tool:search_docs, tool:list_docs, tool:read_doc (V1). tool:* = wildcard pour clients privilégiés.
  • Scopes projets : project:codexia, project:<autre>. project:* = wildcard cross-projets d'un même tenant (jamais cross-tenant, cf. §6).
  • Exemple lecture seule Codexia : ["tool:search_docs", "tool:list_docs", "tool:read_doc", "project:codexia"].
  • Codes d'erreur associés figés au §10.bis : FORBIDDEN_TOOL, FORBIDDEN_PROJECT.

14.4 read_doc — Markdown brut + métadonnées

  • V1 = Markdown brut, tel que stocké, UTF-8. Plus métadonnées (mtime, content_hash, size_bytes). Schéma au §9.
  • Pas de rendu / normalisation côté serveur : un agent LLM lit le Markdown nativement ; toute transformation détruit de l'information utile (ancres, blocs de code, structure). Le rendu est la responsabilité du consommateur final (UI, vue, autre tuyau), pas du serveur MCP.
  • content_hash = même algorithme que le cÅ“ur RAG (cohérence cache et détection de changement).

14.5 Projet « Codexia » — identité BDD, résolution de chemin via source_root du cœur

  • Identité du projet = ligne en table Project (id, tenant_id, slug, root_path, status). C'est l'objet auquel s'accroche la gouvernance (scopes, audit, quotas).
  • Résolution des chemins = on réutilise le source_root configuré dans telaria_rag.yaml (cÅ“ur L0), pas une seconde source de vérité. Project.root_path = métadonnée matérialisée pour l'ADN (vérifier qu'un chemin demandé est sous racine) et l'audit ; en pratique elle pointe sur le même répertoire.
  • search_docs interroge l'index L0 (déjà couplé au source_root) ; read_doc résout path relatif à Project.root_path puis lit le fichier, en passant par les contrôles ADN (inputs/legacy/ interdit en lecture, SCRATCH.md jamais exposé, normalisation du chemin obligatoire).
  • Sélection du projet par le client MCP : champ optionnel project (slug) dans arguments de chaque appel d'outil. Si absent, défaut "codexia" (slug du projet principal V1). Le pipeline §8 résout ensuite tenant + project puis vérifie le scope project:<slug>. (Acté 2026-05-29 — rétro-doc suite implémentation V1 par tlr-mcp. À intégrer aux schémas JSON §9 sous forme d'un champ optionnel commun aux 3 outils dans une révision ultérieure si l'usage le justifie.)

Implications pour l'implémentation

  • Le lot de codage V1 se borne à : stdio + 3 outils + auth opaque + scopes 2-dimensions + Markdown brut + lecture via source_root partagé avec le cÅ“ur. Tout le reste (HTTP, JWT, rendu, racines multiples) est explicitement hors V1.

Documents liés

  • Cadre : specs/ia-vitrine.md
  • CÅ“ur RAG (fournit search_docs) : specs/ia-coeur.md
  • Matériel source — protocole et implémentation : bundles/tlr-mcp.md
  • Fiche agents — agents IA & automatisation : agents/4-interagir-avec-l-ia/4-5-les-agents-ia-et-l-automatisation-de-taches.md

Implémentation

Aspect Localisation
Bundle principal telaria/mcp-bundle (tlr-mcp) — dépôt telaria-mcp
Interfaces ToolInterface (getName, getSchema, execute)
Registre outils ToolRegistry (tag service mcp.tool)
Outils V1 list_docs, read_doc, search_docs (délègue à RetrievalService du cœur L0)
Transport stdio en V1 ; Streamable HTTP prévu V1.1
Auth Token opaque 32 bytes, hash en BDD table mcp_api_client
Commandes CLI bin/console mcp:serve (transport stdio)
Config Variables d'env MCP_TOKEN (stdio) ; config/packages/ dans telaria-app
Multi-tenant Tables mcp_tenant, mcp_api_client, mcp_project, mcp_tool_audit_log dans tlr-mcp

Historique des décisions

Version Date Décision
1.0 2026-06-14 Version initiale — première formalisation du versioning des specs.
— 2026-05-28 Décisions V1 figées : stdio seul (HTTP V1.1), token opaque (pas JWT), scopes tool:<name> ∧ project:<slug>, read_doc = Markdown brut.
— 2026-05-29 tools/list sans token = catalogue statique complet (pas d'auth obligatoire en amont). Frontière de sécurité portée par tools/call. Acté arbitrage Doc Lead suite Q1 codexia #23.
— 2026-05-29 Catalogue d'erreurs figé : JSON-RPC standard + error.data.code stable. Appel métier avant initialize = -32600 sans code LIFECYCLE dédié. Acté suite Q2 codexia #23.
— 2026-06-01 Transmission token stdio via variable MCP_TOKEN (pas argument CLI). Retro-doc suite implémentation V1 tlr-mcp.
— 2026-05-28 Spec consolidée : absorbe codexia-doc-ia.md (gouvernance) et telaria-mcp-tenants.md (multi-tenant), maintenant supprimées.

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.

Loading…
Loading the web debug toolbar…
Attempt #