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
stdioet 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, codesMCP_ERR_*) : non conforme au fil MCP. Les erreurs passent par l'objeterrorJSON-RPC, avec un code applicatif stable danserror.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'objeterror). - 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êteMCP-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(sinon403), liaison localhost en local, authentification obligatoire, gestion de session viaMCP-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 (exclutSCRATCH.mdetinputs/legacy/).read_doc: retourne le contenu d'un document autorisé + ses métadonnées.search_docs: recherche sémantique via leRetrievalServicedu 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 devientsearch_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.mdn'est jamais exposé (cohérent avec.aiignoreet 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 servicemcp.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 leurname,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 partools/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 sesarguments; route vers la méthodeexecutecorrespondante duToolRegistryaprès pipeline. Auth toujours obligatoire (token + scopetool:<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_docsdélègue auRetrievalServicedu 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-Versioninvalide (HTTP) →400. Originnon 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_docd'un chemin hors racine projet → refus (INVALID_PATH).list_docs/search_docsn'incluent jamaisSCRATCH.mdniinputs/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_docssansquery→ erreur de validation (VALIDATION).search_docs: avecRetrievalServicemocké, renvoiekhits triés par score, avecpath/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.codeest stable (contrat client). Leerror.messageest libre (UX) et peut évoluer.-32000est la plage « Server error » réservée par JSON-RPC pour les erreurs applicatives.Cycle de vie (
LIFECYCLE) : un appel métier reçu avantinitializeest traité comme-32600Invalid Request (cf. ligne Protocole ci-dessus), sans code applicatif dédié. Décision : ne pas introduire de codeLIFECYCLEau catalogue —Invalid Requestcouvre 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 dansclaude_desktop_config.json, démonstration desearch_docs/read_doc/list_docssur 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 =
stdioseul. 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, validationOrigin, déploiement surmcp.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'
McpContextreste indépendant du transport.
14.2 Format de token — opaque (32 bytes random base64url), hash en DB
- V1 = token opaque. En
stdiolocal, 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'environnementMCP_TOKENlue au démarrage du processbin/console mcp:serve(transportstdio; le nommcp:stdion'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 dansps/historique shell). Pour HTTP V1.1, headerAuthorization: Bearer <token>standard. (Acté 2026-05-29 — rétro-doc suite implémentation V1 partlr-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_rootconfiguré danstelaria_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_docsinterroge l'index L0 (déjà couplé ausource_root) ;read_docrésoutpathrelatif ÃProject.root_pathpuis lit le fichier, en passant par les contrôles ADN (inputs/legacy/interdit en lecture,SCRATCH.mdjamais exposé, normalisation du chemin obligatoire).- Sélection du projet par le client MCP : champ optionnel
project(slug) dansargumentsde chaque appel d'outil. Si absent, défaut"codexia"(slug du projet principal V1). Le pipeline §8 résout ensuitetenant + projectpuis vérifie le scopeproject:<slug>. (Acté 2026-05-29 — rétro-doc suite implémentation V1 partlr-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 viasource_rootpartagé 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. |