Serveur MCP — référence du composant
Nature
Bundle Symfony headless (pas d'application autonome) exposant la documentation d'un projet comme outils invocables à un client agentique (client MCP de type desktop, IDE, CLI) via le protocole MCP (Model Context Protocol), transporté en JSON-RPC 2.0.
- Namespace public :
Telaria\Mcp\. - S'enfiche dans une application hôte Symfony qui fournit les services d'infrastructure (voir Surface d'intégration).
- S'adosse à un cœur RAG (bundle séparé) pour la recherche sémantique.
- Périmètre V1 : lecture seule (aucun outil d'écriture).
Transports
| Transport | Point d'entrée | Auth |
|---|---|---|
| HTTP Streamable | POST /mcp (JSON-RPC) + GET /mcp (SSE) |
En-tête Authorization: Bearer <token> obligatoire |
| stdio | Commande console mcp:serve |
Variable d'environnement MCP_TOKEN |
Le transport HTTP renvoie toujours un en-tête MCP-Session-Id (repris du client
ou généré côté serveur si absent).
Cycle de vie MCP (handshake)
Méthodes routées :
| Méthode | Traitement |
|---|---|
initialize |
Réponse de capacités (protocolVersion, capabilities.tools, serverInfo). |
notifications/initialized |
Marque la session initialisée (cache, TTL ~30 min) ; aucune réponse. |
tools/list |
Catalogue des outils (auth optionnelle, voir Scopes). |
tools/call |
Pipeline d'exécution complet (voir plus bas). |
| autre | -32601 (méthode inconnue). |
Clients stateless : si le client n'envoie pas d'en-tête de session, le
handshake initialize/initialized est court-circuité et le token fait foi. Les
clients qui envoient un identifiant de session restent soumis au handshake complet.
Réponse initialize :
{ "protocolVersion": "<version protocole, format YYYY-MM-DD>", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "<nom serveur>", "version": "<version>" } }
Outils exposés (V1)
Chaque outil implémente ToolInterface
(getName / getDescription / getSchema / execute) et est découvert par le tag de
service mcp.tool. Les schémas sont en JSON Schema draft 2020-12.
| Outil | Rôle | Dépendance |
|---|---|---|
list_docs |
Liste les documents Markdown du projet (filtre préfixe, limite). | Store vectoriel RAG (pour last_indexed_at) |
read_doc |
Retourne le Markdown brut d'un document + métadonnées. | Garde-fou de chemin |
search_docs |
Recherche sémantique, passages pertinents scorés. | Service de récupération RAG |
list_docs
Entrée : prefix (string, optionnel), limit (integer 1–500, défaut 200).
additionalProperties: false.
Sortie (result.data) :
{ "status": "ok", "corpus": { "ref": "<référence de version du corpus | null>", "ref_date": "<ISO 8601 | null>", "ref_message": "<sujet du dernier commit | null>", "last_indexed_at": "<horodatage d'indexation | null>" }, "documents": [ { "path": "specs/xxx.md", "title": "<H1 ou nom de fichier>" } ], "truncated": false }
- Parcourt récursivement la racine du projet, ne retient que les
.md. - Exclut
scratch.mdet tout chemin sousinputs/legacy/. truncated = truesilimitest atteint.title= premier titre#des premières lignes, sinon le nom de fichier.
read_doc
Entrée : path (string, requis, chemin relatif à la racine).
additionalProperties: false.
Sortie :
{ "status": "ok", "path": "specs/xxx.md", "title": "<H1 ou nom de fichier>", "content": "<markdown brut>", "metadata": { "mtime": "<ISO 8601>", "content_hash": "<sha256 du contenu>", "size_bytes": 1234 } }
pathrésolu par le garde-fou (voir Garde-fou d'accès).- Refus
INVALID_PATHsi : octet nul,scratch.md, sortie de racine, ou sousinputs/legacy/, ou fichier inexistant. Échec de lecture →INTERNAL.
search_docs
Entrée : query (string, requis), k (integer 1–20, défaut 5).
additionalProperties: false.
Sortie :
{ "status": "ok", "hits": [ { "path": "<chemin>", "section": "<section>", "score": 0.87, "excerpt": "<extrait>" } ] }
- Délègue au service de récupération RAG (
retrieve(query, k)), mappe chaque hit vers{path, section, score, excerpt}, tri par score décroissant. - L'isolation par projet côté recherche dépend de la configuration du cœur RAG dans l'application hôte.
Enveloppe de réponse tools/call
En succès, le payload métier est encodé en chaîne JSON dans content[0].text
(double encodage), conforme au format de contenu texte MCP :
{ "jsonrpc": "2.0", "id": "<id>", "result": { "content": [ { "type": "text", "text": "<json du result.data>" } ] } }
Authentification (tokens opaques)
- Le client présente un token brut opaque (aléatoire, encodé base64url sans padding).
- En base, seul le hash SHA-256 du token est stocké ; le token brut n'est jamais persisté.
- Vérifications : token absent/inconnu →
AUTH_REQUIRED; client révoqué ou expiré →TOKEN_REVOKED.
Scopes (double dimension : outil ET projet)
Les scopes sont un tableau JSON porté par le client d'API, ex.
["tool:search_docs", "tool:list_docs", "project:<slug>"].
tool:*accorde tout scope outil ;project:*accorde tout scope projet ; sinon égalité stricte.- À l'appel : refus
FORBIDDEN_TOOLsi le scopetool:<nom>manque. - Résolution du projet : depuis les
argumentsde l'outil, sinon premier scopeproject:<slug>du token. RefusFORBIDDEN_PROJECTsi aucun projet résolu, scope manquant, ou projet(slug, tenant)introuvable (isolation cross-tenant : la recherche est bornée au tenant du client). tools/list: sans token → catalogue complet ; avec token valide → outils filtrés par scopes.
Quotas (rate limiting)
- Quota par couple (tenant, outil) ; consomme 1 jeton par appel.
- Appuyé sur un service de limitation fourni par l'application hôte (fenêtre glissante à la minute).
- Dépassement →
RATE_LIMIT. La limite effective par minute est portée par le client d'API (rateLimitPerMinute, défaut 60).
Audit
Chaque tools/call écrit une entrée d'audit :
tenantId, projectId?, apiClientId?, toolName, status, errorCode?, timestamp.
status∈success|refused|error.refused: erreur métier (scope manquant, quota…),errorCode= code app stable.error: exception non-MCP,errorCode = INTERNAL.- Seul
tools/callest audité (initialize/tools/listne le sont pas).
Pipeline tools/call (7 étapes)
- Valide l'enveloppe JSON-RPC.
- Authentifie + résout tenant/client.
- Vérifie le scope outil.
- Résout le projet (args ou scope) + vérifie le scope projet + existence en base.
- Valide les
argumentscontre l'inputSchema + applique les défauts. - Consomme le quota (tenant, outil).
- Exécute l'outil, puis écrit l'audit.
Codes d'erreur
Codes JSON-RPC (error.code)
| Code | Sens |
|---|---|
-32700 |
Parse error (JSON illisible). |
-32600 |
Requête invalide (enveloppe / appel avant initialize). |
-32601 |
Méthode / outil inconnu. |
-32602 |
Erreur de validation des paramètres. |
-32603 |
Erreur interne. |
-32000 |
Erreurs de gouvernance (auth, scopes, quotas, chemin). |
Codes applicatifs (error.data.code)
Stables, contrat client : AUTH_REQUIRED, TOKEN_REVOKED, FORBIDDEN_TOOL,
FORBIDDEN_PROJECT, INVALID_PATH, RATE_LIMIT, VALIDATION, INTERNAL.
Mapping HTTP (POST /mcp)
FORBIDDEN_TOOL/FORBIDDEN_PROJECT→ 403AUTH_REQUIRED/TOKEN_REVOKED→ 401 (idem token Bearer absent/invalide)- autres erreurs métier → 200 (erreur dans le corps JSON-RPC)
- notification → 204 ; body illisible → 400
Garde-fou d'accès aux fichiers
Résolution/validation de chemin sans toucher au filesystem, dans l'ordre :
- Normalisation des séparateurs.
- Refus si octet nul présent.
- Refus si le nom de base est
scratch.md(insensible à la casse). - Construction du chemin absolu (racine projet + chemin relatif nettoyé).
- Normalisation
./..purement lexicale (ne suit pas les symlinks en V1), portable Windows/Unix. - Refus si le chemin résolu sort de la racine (anti-traversée).
- Refus si le chemin résolu est sous
inputs/legacy/.
Exclusions « ADN » (Accès Données Nécessaires) : scratch.md et inputs/legacy/
sont exclus à la fois de read_doc (refus) et de list_docs (non listés).
Entités de données (mapping ORM)
Quatre entités auto-mappées par l'ORM de l'application hôte :
- Tenant :
name(unique),status. - ApiClient :
tokenHash(SHA-256, unique),scopes(JSON),rateLimitPerMinute(défaut 60),revoked,expiresAt(nullable) ; rattaché à un Tenant. - Project :
slug,rootPath(racine du corpus),status, et métadonnées de version du corpus (corpusRef,corpusRefDate,corpusRefMessage) ; rattaché à un Tenant. Résolution par couple(slug, tenant). - ToolAuditLog : identifiants scalaires (tenant, projet, client),
toolName,status,errorCode,timestamp.
Surface d'intégration (contrats publics)
À implémenter (extension) — ajouter un outil :
ToolInterface:getName(): string,getDescription(): string,getSchema(): array(JSON Schema draft 2020-12),execute(array $args, McpContext $ctx): ToolResult.- Toute implémentation est auto-taggée
mcp.toolet rejoint le registre d'outils.
À fournir (services attendus de l'app hôte) :
- Un gestionnaire d'entités ORM.
- Un pool de cache PSR-6.
- Un service de limitation de débit (rate limiter) configuré côté hôte.
- Le service de récupération RAG et le store vectoriel RAG.
À exposer / router :
- Routes
POST /mcpetGET /mcp. - Mapping Doctrine des entités + migrations (portées par l'app hôte).
Contrats de données stables :
- Codes d'erreur applicatifs.
- Enveloppe MCP
content[].text= JSON encodé. - Format des scopes :
tool:<nom>etproject:<slug>(+ wildcards*).
Validation des paramètres (portée)
Le validateur de schéma est partiel : required, additionalProperties: false, type (string / integer / boolean), minimum / maximum (entiers),
enum, et application des default. Non couvert : formats, tableaux/objets
imbriqués, pattern, minLength, flottants.