Tuto — Un serveur MCP minimal en Symfony
Public visé : développeur Symfony qui veut exposer des outils à un assistant IA via MCP.
Objectif : construire un serveur MCP minimal (transport stdio, JSON-RPC 2.0) exposant un outil, conforme Ă la spec specs/ia-mcp.md.
Concepts : voir la fiche MCP : connecter l'IA Ă vos outils. Pour l'installation sur VPS, voir MCP sur VPS.
Ce qu'on construit
Un serveur stdio qui répond à trois messages MCP :
initialize(poignée de main + capacités),tools/list(catalogue d'outils),tools/call(exécution d'un outil).
On exposera un seul outil pour commencer : ping (puis on branchera search_docs sur le cœur RAG).
1. Le contrat d'un outil
interface ToolInterface { public function getName(): string; // ex. "ping" public function getSchema(): array; // JSON Schema de l'entrée public function execute(array $args): array; // sortie structurée }
final class PingTool implements ToolInterface { public function getName(): string { return 'ping'; } public function getSchema(): array { return [ 'type' => 'object', 'properties' => ['message' => ['type' => 'string']], 'required' => ['message'], 'additionalProperties' => false, ]; } public function execute(array $args): array { return ['status' => 'ok', 'echo' => $args['message'] ?? '']; } }
Les outils sont enregistrés comme services taggés
mcp.toolet collectés dans unToolRegistry(cf. spec §8). Ici, on garde un tableau pour l'exemple.
2. La boucle stdio (JSON-RPC 2.0)
Le transport stdio lit une requête JSON par ligne sur stdin et écrit la réponse sur stdout ; stderr est réservé aux logs.
// bin/mcp-server (commande console ou script) $tools = ['ping' => new PingTool()]; while (($line = fgets(STDIN)) !== false) { $req = json_decode(trim($line), true); if (!is_array($req) || ($req['jsonrpc'] ?? null) !== '2.0') { fwrite(STDOUT, encodeError(null, -32600, 'Invalid Request') . "\n"); continue; } $id = $req['id'] ?? null; $result = match ($req['method'] ?? '') { 'initialize' => [ 'protocolVersion' => '2025-11-25', 'capabilities' => ['tools' => new stdClass()], 'serverInfo' => ['name' => 'tlr-mcp', 'version' => '0.1.0'], ], 'tools/list' => ['tools' => array_map( fn (ToolInterface $t) => [ 'name' => $t->getName(), 'inputSchema' => $t->getSchema(), ], array_values($tools) )], 'tools/call' => callTool($tools, $req['params'] ?? []), default => null, }; if ($id === null) { // notification : pas de réponse continue; } fwrite(STDOUT, json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result]) . "\n"); } function callTool(array $tools, array $params): array { $name = $params['name'] ?? ''; $args = $params['arguments'] ?? []; if (!isset($tools[$name])) { return ['isError' => true, 'content' => [['type' => 'text', 'text' => "Outil inconnu : $name"]]]; } $out = $tools[$name]->execute($args); return ['content' => [['type' => 'text', 'text' => json_encode($out, JSON_UNESCAPED_UNICODE)]]]; } function encodeError(?int $id, int $code, string $msg): string { return json_encode(['jsonrpc' => '2.0', 'id' => $id, 'error' => ['code' => $code, 'message' => $msg]]); }
Squelette pédagogique : en production, on s'appuie sur le
ToolRegistry, on valide l'inputSchema, on applique scopes/ADN/audit (spec §6, §8) et on gère le cycle de vie complet (notifications/initialized).
3. Tester Ă la main
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ | php bin/mcp-server printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ | php bin/mcp-server printf '%s\n' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ping","arguments":{"message":"bonjour"}}}' \ | php bin/mcp-server
On doit voir, ligne par ligne : la réponse d'initialize, le catalogue (ping), puis l'écho.
4. Brancher search_docs sur le cœur
Une fois la mécanique en place, on remplace PingTool par un SearchDocsTool qui délègue au RetrievalService du cœur RAG (specs/ia-coeur.md) : l'outil reçoit {query, k}, renvoie les passages avec score et source. C'est l'ancre V1 de la spec MCP.
Bonnes pratiques retenues
- stdout = MCP uniquement : aucun
echo/var_dumpparasite ; logs surstderr. - Schémas stricts (
additionalProperties: false) et sorties structurées. - Sécurité d'abord : scopes, périmètre projet, humain dans la boucle pour les écritures (V2).
Pour aller plus loin
- Spec MCP :
specs/ia-mcp.md - Brancher le serveur sur un client : Brancher MCP sur Claude Desktop / Cursor
- Concepts : fiche 4.6 — MCP