reference/serveur-mcp.md

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.md et tout chemin sous inputs/legacy/.
  • truncated = true si limit est 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
  }
}
  • path résolu par le garde-fou (voir Garde-fou d'accès).
  • Refus INVALID_PATH si : octet nul, scratch.md, sortie de racine, ou sous inputs/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_TOOL si le scope tool:<nom> manque.
  • Résolution du projet : depuis les arguments de l'outil, sinon premier scope project:<slug> du token. Refus FORBIDDEN_PROJECT si 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/call est audité (initialize / tools/list ne le sont pas).

Pipeline tools/call (7 étapes)

  1. Valide l'enveloppe JSON-RPC.
  2. Authentifie + résout tenant/client.
  3. Vérifie le scope outil.
  4. Résout le projet (args ou scope) + vérifie le scope projet + existence en base.
  5. Valide les arguments contre l'inputSchema + applique les défauts.
  6. Consomme le quota (tenant, outil).
  7. 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 → 403
  • AUTH_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 :

  1. Normalisation des séparateurs.
  2. Refus si octet nul présent.
  3. Refus si le nom de base est scratch.md (insensible à la casse).
  4. Construction du chemin absolu (racine projet + chemin relatif nettoyé).
  5. Normalisation ./.. purement lexicale (ne suit pas les symlinks en V1), portable Windows/Unix.
  6. Refus si le chemin résolu sort de la racine (anti-traversée).
  7. 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.tool et 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 /mcp et GET /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> et project:<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.

Assistant documentaire

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