2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPの1ファイルでClaude Code用のMCPサーバーを作る

Last updated at Posted at 2025-06-25

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のエラーについてはコピペして聞いたら結構正確に教えてくれた。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?