[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"article:fr:mcp-server-php":3},{"html":4,"lang":5},"\u003Ch2>MCP : donner des mains à un assistant IA\u003C\u002Fh2>\n\u003Cp>Le \u003Cstrong>Model Context Protocol\u003C\u002Fstrong> (MCP) est un protocole ouvert qui standardise la façon dont une application d'IA (Claude, un IDE, un agent) se connecte à des sources de données et à des actions externes. Au lieu d'écrire une intégration spécifique pour chaque assistant, vous écrivez \u003Cstrong>un serveur MCP\u003C\u002Fstrong>, et tous les clients compatibles savent l'utiliser.\u003C\u002Fp>\n\u003Cp>Un serveur MCP expose trois types de capacités :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Tools\u003C\u002Fstrong> : des actions que le modèle peut appeler (vérifier un service, créer un ticket, lancer une requête SQL en lecture…)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Resources\u003C\u002Fstrong> : des données que le client peut lire (configuration, documentation, état d'un serveur)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Prompts\u003C\u002Fstrong> : des modèles de messages réutilisables, paramétrables par l'utilisateur\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Le client et le serveur échangent des messages \u003Cstrong>JSON-RPC 2.0\u003C\u002Fstrong>, soit via l'entrée\u002Fsortie standard (\u003Ccode>stdio\u003C\u002Fcode>, idéal en local), soit via HTTP (Streamable HTTP, pour un serveur distant).\u003C\u002Fp>\n\n\u003Ch3>Le SDK PHP officiel\u003C\u002Fh3>\n\u003Cp>Depuis 2025, PHP dispose d'un SDK officiel : \u003Ccode>mcp\u002Fsdk\u003C\u002Fcode>, développé conjointement par la \u003Cstrong>PHP Foundation\u003C\u002Fstrong> et le \u003Cstrong>projet Symfony\u003C\u002Fstrong>, à partir du travail de PHP-MCP et de Symfony AI. Il est agnostique du framework et suit la promesse de rétrocompatibilité de Symfony. Il reste marqué expérimental avant sa version 1.0 : figez la version dans votre \u003Ccode>composer.json\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Prérequis : PHP 8.1 minimum. Installation :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>composer require mcp\u002Fsdk symfony\u002Ffinder\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>Piège n°1\u003C\u002Fstrong> : \u003Ccode>symfony\u002Ffinder\u003C\u002Fcode> n'est qu'une dépendance \u003Cem>suggérée\u003C\u002Fem>, mais elle est indispensable à la découverte automatique des outils par attributs. Sans elle, le serveur échoue au démarrage… et comme l'erreur part sur la sortie d'erreur, le client voit simplement un serveur sans aucun outil.\u003C\u002Fp>\n\n\u003Ch3>Un premier serveur : un assistant DevOps\u003C\u002Fh3>\n\u003Cp>Construisons un serveur utile au quotidien : il vérifie qu'une URL répond, contrôle l'espace disque, expose des informations système et propose un prompt de rapport d'incident. Commencez par déclarer l'autoload de vos classes :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n    \"require\": {\n        \"mcp\u002Fsdk\": \"^0.8\",\n        \"symfony\u002Ffinder\": \"^8.1\"\n    },\n    \"autoload\": {\n        \"psr-4\": { \"App\\\\\": \"src\u002F\" }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Les capacités sont de simples méthodes PHP annotées. Le SDK génère le schéma JSON des paramètres à partir des types PHP, et la description à partir du docblock :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;?php\n\nnamespace App;\n\nuse Mcp\\Capability\\Attribute\\McpPrompt;\nuse Mcp\\Capability\\Attribute\\McpResource;\nuse Mcp\\Capability\\Attribute\\McpTool;\nuse Mcp\\Capability\\Attribute\\Schema;\nuse Mcp\\Exception\\ToolCallException;\n\nfinal class DevOpsTools\n{\n    \u002F**\n     * Vérifie qu'une URL répond et renvoie son code HTTP et son temps de réponse.\n     *\u002F\n    #[McpTool(name: 'check_url')]\n    public function checkUrl(\n        #[Schema(format: 'uri', description: 'URL complète, ex. https:\u002F\u002Fbenmacha.tn')]\n        string $url,\n    ): array {\n        if (!preg_match('#^https?:\u002F\u002F#', $url)) {\n            throw new ToolCallException('Seules les URL http(s) sont acceptées.');\n        }\n\n        $start = microtime(true);\n        $context = stream_context_create(['http' =&gt; ['method' =&gt; 'HEAD', 'timeout' =&gt; 5, 'ignore_errors' =&gt; true]]);\n        $headers = @get_headers($url, true, $context);\n\n        if ($headers === false) {\n            return ['url' =&gt; $url, 'up' =&gt; false, 'error' =&gt; 'Hôte injoignable'];\n        }\n\n        preg_match('#\\s(\\d{3})\\s#', $headers[0], $m);\n        $status = (int) ($m[1] ?? 0);\n\n        return [\n            'url' =&gt; $url,\n            'up' =&gt; $status &gt; 0 &amp;&amp; $status &lt; 400,\n            'status' =&gt; $status,\n            'time_ms' =&gt; (int) round((microtime(true) - $start) * 1000),\n        ];\n    }\n\n    \u002F**\n     * Retourne l'espace disque utilisé et disponible pour un chemin.\n     *\u002F\n    #[McpTool(name: 'disk_usage')]\n    public function diskUsage(\n        #[Schema(description: 'Chemin à analyser')]\n        string $path = '\u002F',\n    ): array {\n        $total = @disk_total_space($path);\n        $free = @disk_free_space($path);\n\n        if ($total === false || $free === false) {\n            throw new ToolCallException(sprintf('Chemin illisible : %s', $path));\n        }\n\n        return [\n            'path' =&gt; $path,\n            'total_gb' =&gt; round($total \u002F 1e9, 1),\n            'free_gb' =&gt; round($free \u002F 1e9, 1),\n            'used_percent' =&gt; round(100 * ($total - $free) \u002F $total, 1),\n        ];\n    }\n\n    #[McpResource(uri: 'server:\u002F\u002Finfo', name: 'server_info', mimeType: 'application\u002Fjson')]\n    public function serverInfo(): array\n    {\n        return ['hostname' =&gt; gethostname(), 'os' =&gt; PHP_OS_FAMILY, 'php' =&gt; PHP_VERSION];\n    }\n\n    \u002F**\n     * Prépare un rapport d'incident à partir d'un service et d'un symptôme.\n     *\u002F\n    #[McpPrompt(name: 'incident_report')]\n    public function incidentReport(string $service, string $symptom): array\n    {\n        return [[\n            'role' =&gt; 'user',\n            'content' =&gt; \"Le service « $service » présente ce symptôme : $symptom. \"\n                . \"Utilise check_url et disk_usage pour diagnostiquer, puis rédige un rapport \"\n                . \"d'incident court : impact, cause probable, actions immédiates.\",\n        ]];\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le point d'entrée tient en quelques lignes : on déclare le serveur, on lui demande de scanner le dossier \u003Ccode>src\u003C\u002Fcode>, et on le lance sur le transport stdio.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>#!\u002Fusr\u002Fbin\u002Fenv php\n&lt;?php\n\nrequire __DIR__.'\u002Fvendor\u002Fautoload.php';\n\nuse Mcp\\Server;\nuse Mcp\\Server\\Transport\\StdioTransport;\n\nexit(Server::builder()\n    -&gt;setServerInfo('DevOps Assistant', '1.0.0')\n    -&gt;setDiscovery(__DIR__, ['src'])\n    -&gt;build()\n    -&gt;run(new StdioTransport()));\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Ce que voit le client\u003C\u002Fh3>\n\u003Cp>À partir de la signature \u003Ccode>checkUrl(string $url)\u003C\u002Fcode>, du docblock et de l'attribut \u003Ccode>#[Schema]\u003C\u002Fcode>, le SDK publie cette définition d'outil :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"name\": \"check_url\",\n  \"description\": \"Vérifie qu'une URL répond et renvoie son code HTTP et son temps de réponse.\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"url\": { \"type\": \"string\", \"format\": \"uri\", \"description\": \"URL complète, ex. https:\u002F\u002Fbenmacha.tn\" }\n    },\n    \"required\": [\"url\"]\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Pour \u003Ccode>disk_usage\u003C\u002Fcode>, la valeur par défaut \u003Ccode>'\u002F'\u003C\u002Fcode> devient un \u003Ccode>\"default\"\u003C\u002Fcode> et le paramètre n'est pas obligatoire. Quand un outil retourne un tableau, le SDK le renvoie à la fois en texte JSON et en \u003Ccode>structuredContent\u003C\u002Fcode>, directement exploitable par le client.\u003C\u002Fp>\n\n\u003Ch3>Gérer les erreurs correctement\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>Piège n°2\u003C\u002Fstrong> : n'importe quelle exception ne convient pas. Une exception quelconque (\u003Ccode>InvalidArgumentException\u003C\u002Fcode>, \u003Ccode>RuntimeException\u003C\u002Fcode>…) est transformée en erreur JSON-RPC générique, « Error while executing tool » : le modèle ne sait pas ce qui s'est passé. Levez plutôt une \u003Ccode>Mcp\\Exception\\ToolCallException\u003C\u002Fcode> : le message est renvoyé dans un résultat marqué \u003Ccode>isError: true\u003C\u002Fcode>, que le modèle peut lire pour corriger son appel (par exemple, réessayer avec une URL en https).\u003C\u002Fp>\n\n\u003Ch3>Tester sans assistant : le client PHP et l'Inspector\u003C\u002Fh3>\n\u003Cp>Le SDK contient aussi un client, parfait pour des tests automatisés :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>use Mcp\\Client;\nuse Mcp\\Client\\Transport\\StdioTransport;\n\n$client = Client::builder()-&gt;setClientInfo('Tests', '1.0.0')-&gt;build();\n$client-&gt;connect(new StdioTransport(command: 'php', args: [__DIR__.'\u002Fserver.php']));\n\nforeach ($client-&gt;listTools()-&gt;tools as $tool) {\n    echo $tool-&gt;name, ' : ', $tool-&gt;description, PHP_EOL;\n}\n\n$result = $client-&gt;callTool('check_url', ['url' =&gt; 'https:\u002F\u002Fbenmacha.tn']);\nvar_dump($result-&gt;structuredContent); \u002F\u002F ['url' =&gt; ..., 'up' =&gt; true, 'status' =&gt; 200, 'time_ms' =&gt; ...]\n\n$client-&gt;disconnect();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Pour explorer le serveur visuellement, l'\u003Cstrong>MCP Inspector\u003C\u002Fstrong> officiel lance le serveur et affiche outils, ressources et prompts :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>npx @modelcontextprotocol\u002Finspector php server.php\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Brancher le serveur sur Claude\u003C\u002Fh3>\n\u003Cp>Avec \u003Cstrong>Claude Code\u003C\u002Fstrong>, une seule commande suffit :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>claude mcp add devops -- php \u002Fchemin\u002Fabsolu\u002Fvers\u002Fserver.php\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Avec \u003Cstrong>Claude Desktop\u003C\u002Fstrong>, ajoutez le serveur dans \u003Ccode>claude_desktop_config.json\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"mcpServers\": {\n    \"devops\": {\n      \"command\": \"php\",\n      \"args\": [\"\u002Fchemin\u002Fabsolu\u002Fvers\u002Fserver.php\"]\n    }\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Demandez ensuite : « benmacha.tn répond-il correctement, et reste-t-il de la place sur le disque ? ». L'assistant appelle \u003Ccode>check_url\u003C\u002Fcode> puis \u003Ccode>disk_usage\u003C\u002Fcode>, et synthétise les résultats.\u003C\u002Fp>\n\n\u003Ch3>Les règles d'or en production\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Ne jamais écrire sur stdout\u003C\u002Fstrong> en mode stdio : la sortie standard est réservée au protocole. Un \u003Ccode>echo\u003C\u002Fcode> ou un \u003Ccode>var_dump\u003C\u002Fcode> oublié corrompt les échanges. Loggez sur stderr ou dans un fichier (le builder accepte un logger PSR-3).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Des outils étroits plutôt qu'un outil « exécuter une commande »\u003C\u002Fstrong> : exposer un shell ou du SQL libre revient à donner les clés du serveur au modèle.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Valider chaque entrée\u003C\u002Fstrong> : le schéma JSON aide le modèle, mais ne remplace pas la validation côté serveur (listes blanches de chemins, d'hôtes, de tables).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Moindre privilège\u003C\u002Fstrong> : faites tourner le serveur avec un utilisateur système dédié et un compte de base de données en lecture seule quand c'est possible.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Des descriptions soignées\u003C\u002Fstrong> : c'est la seule documentation que lit le modèle pour choisir le bon outil et le bon paramètre.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Aller plus loin\u003C\u002Fh3>\n\u003Cp>Le SDK fournit aussi un transport HTTP (Streamable HTTP) pour héberger un serveur distant partagé par une équipe, avec gestion des sessions et de l'autorisation, et il prend en charge les deux générations du protocole, y compris la révision sans état \u003Ccode>2026-07-28\u003C\u002Fcode>. Côté frameworks, \u003Ccode>symfony\u002Fmcp-bundle\u003C\u002Fcode> intègre le SDK à Symfony (vos services deviennent des outils MCP, avec l'injection de dépendances), et \u003Ccode>api-platform\u002Fmcp\u003C\u002Fcode> expose directement vos ressources API Platform.\u003C\u002Fp>\n\u003Cp>C'est l'approche que j'utilise pour connecter des assistants IA aux données métier : quelques outils bien délimités, en lecture seule, avec des descriptions précises, et l'IA devient capable de répondre à des questions qui demandaient auparavant une requête SQL ou un export manuel.\u003C\u002Fp>\n","fr",1790543096364]