MCPサーバーとはなんぞや?ということを知るために、簡単なMCPサーバーを作ってみたい。
情報が全然なかったが、素晴らしい記事があった。
httpとsseの違いがよく分かってないのだが、そのまま持ってきても動かなかったので動くように試行錯誤。
サーバー?というか普通のWeb。httpでもサーバーと呼ぶのかな?
Claude Code のデバッグモードでエラーが出ずに動くところまではいけた。
index.php
<?php
enum ErrorCode: int
{
// Standard JSON-RPC error codes
case ParseError = -32700;
case InvalidRequest = -32600;
case MethodNotFound = -32601;
case InvalidParams = -32602;
case InternalError = -32603;
}
/**
* @phpstan-type Request array{
* jsonrpc?:string,
* id?: int,
* method?: string,
* params?: array{name?:string},
* protocolVersion?: string,
* clientInfo?: array{name:string,version:string},
* }
*/
class McpTimeServer
{
private const TOOL_NAME = 'DateTime_Tool';
private const JSON_RPC_VERSION = '2.0';
private static function doGet(): void
{
self::respond([
'status' => 'OK',
'message' => 'MCP server is running',
]);
}
/** @param Request $request */
private static function doPost($request): void
{
$response = [];
try {
$result = match ($request['method'] ?? '') {
'initialize' => self::initialize(),
'notifications/initialized' => (object)[],
'resources/list' => ['resources' => []],
'prompts/list' => ['prompts' => []],
'tools/list' => self::listMcpTools(),
'tools/call' => self::callDateTimeTool($request['params'] ?? []),
default => throw new \Exception('Method is not found.', ErrorCode::MethodNotFound->value),
};
$response = [
'result' => $result,
];
} catch (Exception $e){
$response = [
'error' => [
'code' => $e->getCode(),
'message' => $e->getMessage(),
]
];
} finally {
if (isset($request['id']))
$response['id'] = $request['id'];
self::respond($response);
}
}
/** @return array<string,mixed> */
private static function initialize()
{
return [
'protocolVersion' => '2025-03-26',
'capabilities' => [
'logging' => (object)[],
'tools' => [
'listChanged' => false,
],
'resources' => (object)[],
'prompts' => (object)[],
],
'serverInfo' => [
'name' => 'PHP DateTime Server',
'version' => '1.0.0',
],
];
}
/** @return array<string,mixed> */
private static function listMcpTools()
{
return [
'tools' => [
[
'name' => self::TOOL_NAME,
'description' => '現在時刻を返す',
'inputSchema' => [
'type' => 'object',
'properties' => (object)[],
'required' => [],
],
],
]
];
}
/**
* @param array{name?:string} $params
* @return array<string,mixed>
*/
private static function callDateTimeTool($params)
{
if (!isset($params['name']))
throw new \Exception('Tool is not specified', ErrorCode::InvalidParams->value);
if ($params['name'] !== self::TOOL_NAME)
throw new \Exception('Unknown tool: ' . $params['name'], ErrorCode::InvalidParams->value);
return [
'content' => [
[
'type' => 'text',
'text' => date('Y-m-d H:i:s'),
],
],
'isError' => false,
];
}
/** @param array<mixed> $response */
private static function respond($response): void
{
//header("Content-Type: text/event-stream");
header("Content-Type: application/json");
header("Cache-Control: no-cache");
$response['jsonrpc'] = self::JSON_RPC_VERSION;
// file_put_contents('/tmp/mcp-response.txt', var_export($response, true), FILE_APPEND);
echo json_encode($response);
}
public static function run(): void
{
// file_put_contents('/tmp/mcp-log.txt', var_export($_SERVER, true), FILE_APPEND);
if ($_SERVER['REQUEST_METHOD'] == 'GET'){
self::doGet();
}elseif ($_SERVER['REQUEST_METHOD'] == 'POST'){
$json = file_get_contents('php://input');
$data = json_decode($json ?: '{}', true);
// file_put_contents('/tmp/mcp-request.txt', var_export($data, true), FILE_APPEND);
/** @var Request $data */
self::doPost($data);
}
}
}
try {
date_default_timezone_set('Asia/Tokyo');
McpTimeServer::run();
} catch (\Exception $e){
// file_put_contents('/tmp/mcp-error.txt', $e->getMessage(), FILE_APPEND);
}
これを手元のサーバーに置く。
claude mcp add --transport http phpdate http://localhost/mcp/datetime
これで使えるようになる。
プロジェクト限定にしたいときは
claude mcp add --transport http -s project phpdate http://localhost/mcp/datetime
こっち。.mcp.json
が生成される。
初期設定やプロトコルについてはClaude Codeは正しく答えてくれなかったが、型やjsonのエラーについてはコピペして聞いたら結構正確に教えてくれた。