Le Model Context Protocol (MCP) est devenu le standard pour exposer des données et des actions à des LLM. La spec est élégante mais la doc officielle plonge vite dans les SDK TypeScript et Python. Ce post déballe le minimum côté PHP, sans dépendance.
Le protocole en 30 secondes
MCP est un wrapper JSON-RPC 2.0 sur un transport. Le client (Claude Desktop, un agent custom, etc.) envoie des requêtes du genre tools/list ou tools/call. Le serveur répond en JSON. Trois transports existent : stdio (subprocess), HTTP+SSE, et HTTP streamable plus récent. On part sur stdio, le plus simple.
Le code
Voici un serveur complet qui expose un tool get_time. Sauvegarde dans mcp-server.php :
<?php
$tools = [
'get_time' => [
'description' => 'Retourne l\'heure courante en ISO 8601',
'inputSchema' => ['type' => 'object', 'properties' => (object) []],
'handler' => fn() => date('c'),
],
];
while (($line = fgets(STDIN)) !== false) {
$req = json_decode(trim($line), true);
$id = $req['id'] ?? null;
$method = $req['method'] ?? '';
$params = $req['params'] ?? [];
$result = match ($method) {
'initialize' => ['protocolVersion' => '2024-11-05',
'capabilities' => ['tools' => (object) []],
'serverInfo' => ['name' => 'php-demo', 'version' => '0.1']],
'tools/list' => ['tools' => array_map(
fn($name, $t) => ['name' => $name, 'description' => $t['description'],
'inputSchema' => $t['inputSchema']],
array_keys($tools), $tools)],
'tools/call' => (function () use ($tools, $params) {
$name = $params['name'];
$args = $params['arguments'] ?? [];
$out = $tools[$name]['handler']($args);
return ['content' => [['type' => 'text', 'text' => (string) $out]]];
})(),
default => null,
};
if ($id !== null) {
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result]) . "\n";
}
}
Le tester
Configure Claude Desktop ou ton client MCP avec ce serveur (commande : php /chemin/mcp-server.php). Il devrait lister un tool get_time et l'appeler répond l'heure courante.
Ce qui manque pour un vrai serveur
- Gestion d'erreurs JSON-RPC propres (codes -32700, -32601, etc.)
- Validation du schéma d'entrée avant d'appeler le handler
- Capabilities supplémentaires : resources, prompts, sampling
- Transport HTTP streamable pour les déploiements serverless
- Logs séparés (vers stderr, jamais stdout qui sert au protocole)
Mais le squelette ci-dessus suffit pour comprendre la mécanique. À partir de là, tu peux exposer une vraie DB, une API métier, ou un tool de calcul. Le format reste le même.