Construire un serveur MCP en PHP/Symfony : exposez vos outils aux agents IA
Le Model Context Protocol (MCP) s'impose rapidement comme le standard d'interopérabilité entre les agents IA et les applications métier. Si vous avez déjà connecté Claude Desktop à un serveur MCP, vous étiez côté client. Cet article vous montre comment construire l'autre côté : un serveur MCP robuste exposant vos outils PHP à n'importe quel agent compatible.
Note de version : Les exemples ci-dessous ciblent la spécification MCP
2025-11-25. Le protocole a connu des changements cassants entre versions (noms de champs, codes d'erreur). Vérifiez toujours leprotocolVersioncontre le changelog officiel avant tout déploiement.
Cet article s'inspire de Building a Production MCP Server in Laravel par Dewald Hugo, adapté ici pour l'écosystème PHP/Symfony.
Qu'est-ce que MCP — et ce que ce n'est pas
MCP est un protocole JSON-RPC 2.0. Pas un standard REST, pas un framework. Il définit une conversation structurée entre deux parties :
- Le client (un agent IA comme Claude) demande la liste des outils disponibles.
- Le serveur (votre application PHP) les décrit.
- Le client demande l'exécution d'un outil avec des paramètres.
- Le serveur valide, exécute, et retourne le résultat.
Cette simplicité est trompeuse. Derrière ce flux se cachent des exigences précises : schémas JSON Schema pour chaque outil, gestion des erreurs normalisée, et un handshake d'initialisation obligatoire.
// Exemple de requête JSON-RPC MCP
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_articles",
"arguments": {
"query": "symfony messenger",
"limit": 5
}
}
}
Structurer le serveur MCP en Symfony
Le point d'entrée HTTP
Toutes les requêtes MCP transitent par un unique endpoint POST. En Symfony, cela se traduit par un contrôleur dédié qui dispatche selon la méthode JSON-RPC reçue :
// src/Controller/McpController.php
#[Route('/mcp', methods: ['POST'])]
public function handle(Request $request): JsonResponse
{
$payload = json_decode($request->getContent(), true);
return match ($payload['method']) {
'initialize' => $this->handleInitialize($payload),
'tools/list' => $this->handleToolsList($payload),
'tools/call' => $this->handleToolsCall($payload),
default => $this->methodNotFound($payload['id'] ?? null),
};
}
Déclarer vos outils
Chaque outil est décrit par un nom, une description lisible par l'IA, et un schéma JSON Schema pour ses paramètres. Cette description est critique : c'est ce que l'agent lit pour décider quand et comment appeler votre outil.
private function handleToolsList(array $payload): JsonResponse
{
return new JsonResponse([
'jsonrpc' => '2.0',
'id' => $payload['id'],
'result' => [
'tools' => [
[
'name' => 'search_articles',
'description' => 'Recherche des articles dans la base de connaissance par mots-clés.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'query' => [
'type' => 'string',
'description' => 'Termes de recherche',
],
'limit' => [
'type' => 'integer',
'default' => 10,
],
],
'required' => ['query'],
],
],
],
],
]);
}
Champ
inputSchemavsparameters: La spec2025-11-25utiliseinputSchema. Des versions antérieures utilisaientparameters. Ce type de changement cassant justifie de versionner strictement votre implémentation.
Authentification et sécurité
Un serveur MCP exposé publiquement doit être protégé. Deux approches courantes :
Bearer Token (recommandé pour débuter)
private function authenticate(Request $request): bool
{
$token = $request->headers->get('Authorization', '');
return hash_equals(
'Bearer ' . $_ENV['MCP_SECRET_TOKEN'],
$token
);
}
OAuth 2.0 pour les environnements multi-clients — la spec MCP décrit un flux d'autorisation complet, particulièrement utile si plusieurs agents tiers doivent accéder à votre serveur avec des permissions différenciées.
Quelques règles non négociables :
- Toujours HTTPS en production.
- Valider chaque paramètre entrant avec les contraintes du schéma déclaré.
- Ne jamais exposer de stack traces dans les réponses d'erreur MCP.
- Logger toutes les invocations d'outils pour l'audit.
Gestion des erreurs : le format JSON-RPC
Les erreurs MCP suivent le format JSON-RPC standard. Ne renvoyez jamais une réponse HTTP 500 brute — encapsulez toujours dans la structure attendue :
private function buildError(mixed $id, int $code, string $message): JsonResponse
{
return new JsonResponse([
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => $code,
'message' => $message,
],
]);
}
// Codes standards JSON-RPC
// -32700 : Parse error
// -32600 : Invalid Request
// -32601 : Method not found
// -32602 : Invalid params
// -32603 : Internal error
Un agent IA qui reçoit une erreur mal formatée peut simplement abandonner la tâche ou boucler indéfiniment. La rigueur sur les erreurs est aussi importante que la logique métier.
Ce qu'il faut retenir
Construire un serveur MCP en PHP n'est pas plus complexe que construire une API REST bien conçue — c'est une question de rigueur sur le protocole. Les points essentiels :
- Un endpoint unique qui dispatche sur la méthode JSON-RPC.
- Des descriptions d'outils soignées : l'agent prend ses décisions sur cette base.
- Une validation stricte des paramètres entrants avant toute exécution.
- Un format d'erreur normalisé pour que les agents puissent gérer les échecs proprement.
- Un suivi de version de la spec MCP — le protocole évolue et les changements sont cassants.
Avec Symfony, la structure en services, la validation via le Validator component, et le système d'événements offrent une base solide pour aller plus loin : outils dynamiques chargés depuis la base de données, rate limiting par agent, télémétrie OpenTelemetry sur chaque invocation.
Le MCP transforme votre application PHP en une surface d'action directement adressable par les LLM. C'est une brique d'architecture qui mérite d'être posée solidement dès le départ.