初めに
昨今話題のMCPサーバですが、PHP用のSDKはまだ提供されていません。
PHPで作りたかったので、普段使用しているFlowというWebフレームワークでMCPサーバを実装してみました。
設計
実装前に、MCPサーバの理解も含め、設計をしていきましょう。
MCPサーバの種類
MCPサーバには標準入出力で実行を行うSTDIOと、HTTPで通信を行うSSEの二種類があります。今回はSSEで実装しました。
エンドポイント
MCPサーバは様々な用途のリクエストが飛んできますが、URL自体は全て同じです。
今回はhttp://localhost:8081/sse
となるように実装しました。
リクエスト
MCPクライアントから送られてくるリクエストは大きく分けて5つあります。
- MCPクライアントとMCPサーバとの初回疎通確認 (POST)
- 疎通確認完了通知 (POST)
- MCPサーバのツール一覧取得 (POST)
- MCPサーバのツール実行 (POST)
- 定期的なヘルスチェック用リクエスト (GET)
POSTは「MCPのライフサイクル処理 & ツール実行」
前述の通り、MCPサーバのURLは一つだけですが、POSTでは様々なリクエストが送られてきます。
そこで、MCPサーバは、リクエストパラメータのmethod
の値を見て、どの用途でリクエストされているのかを判定します。
以下はリクエストBodyの例です。tools/list
からツール一覧取得用のリクエストであることがわかります。
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": "optional-cursor-value"
}
}
各リクエストでどんなパラメータが送られてくるかはドキュメントをご覧ください。
GETは「定期的なヘルスチェック用」
MCPサーバには、ヘルスチェックのようなリクエストが定期的に届きます。
このリクエストはGETで来ます。レスポンスの形式は何でもよく、200応答であれば良さそうです。
$ curl -X GET 'http://localhost:8081/sse'
{"status":"OK","message":"MCP server is running"}
実装
実際にMCPサーバを作成していきましょう。
今回作成するのは、「名前からあだ名を生成するMCPサーバ」です。
※MCPクライアントとの疎通確認がメインなので、あだ名は固定値で返します。
今回作成したコードはGitHubに上げましたので、よければご覧ください。
スコープ
今回はプロトコルの理解を目的とし、以下の方針で実装しました。
- 疎通ができる最小限のMCPサーバを実装する
- バリデーションやエラーハンドリングなど、詳細な機能は実装しない
環境
・言語:PHP 8.2.17
・フレームワーク:Flow 8.3系
ディレクトリ構成
ディレクトリ構成は以下です。
flow-mcp
└ Packages/
├ Application/
| └ NNHKRNK.MCP/
| ├ Classes/
| | └ Controller/
| | └ McpController.php (1)
| └ Configuration/
| └ Routes.yaml (2)
├ Framework/
└ Libraries/
今回は以下のファイルを作成しました。
- McpController.php
- Routes.yaml
McpController.php
MCPサーバのエンドポイント実装です。
<?php
namespace NNHKRNK\MCP\Controller;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Mvc\View\JsonView;
class McpController extends ActionController
{
/**
* レスポンスをJSON形式で返すためのビュー
* @Flow\Inject
* @var \Neos\Flow\Mvc\View\JsonView
*/
protected $view;
/**
* MCPサーバがサポートするメディアタイプ
*/
protected $supportedMediaTypes = ['text/html', 'application/json', 'application/xml', 'application/yaml', 'text/event-stream'];
/**
* MCPサーバが使用するJSON-RPCのバージョン
*/
const JSON_RPC_VERSION = '2.0';
/**
* MCPサーバのメソッド名定数
*/
const MCP_METHOD_INITIALIZE = 'initialize';
const MCP_METHOD_NOTIFICATIONS_INITIALIZED = 'notifications/initialized';
const MCP_METHOD_TOOLS_LIST = 'tools/list';
const MCP_METHOD_TOOLS_CALL = 'tools/call';
/**
* ヘルスチェック用エンドポイント
* MCPクライアントからのヘルスチェックを受け付けるアクション
*
* @return void
*/
public function healthCheckAction(): void
{
$this->view->assign('value', [
'status' => 'OK',
'message' => 'MCP server is running',
]);
}
/**
* MCPサーバ用エンドポイント.
*
* @return void
*/
public function handleMcpAction(): void
{
/**
* リクエストパラメータを取得
*
* @var array{
* jsonrpc: string,
* id?: int,
* method: string,
* params?: array<mixed>
* } $arguments
*/
$arguments = $this->request->getArguments();
$response['jsonrpc'] = self::JSON_RPC_VERSION;
if (array_key_exists('id', $arguments)) {
$response['id'] = $arguments['id'];
}
try {
$response['result'] = match ($arguments['method']) {
self::MCP_METHOD_INITIALIZE => $this->initMcp(),
self::MCP_METHOD_NOTIFICATIONS_INITIALIZED => [],
self::MCP_METHOD_TOOLS_LIST => $this->listMcpTools(),
self::MCP_METHOD_TOOLS_CALL => $this->callMcpTool($arguments['params']),
};
} catch (\Exception $e) {
$response['error'] = [
'code' => $e->getCode(),
'message' => $e->getMessage(),
];
} finally {
$this->view->assign('value', $response);
}
}
/**
* MCPサーバの初期化
*/
public function initMcp(): array
{
return [
'protocolVersion' => '2025-03-26',
'capabilities' => [
'logging' => [],
'tools' => [
'listChanged' => false,
],
'resources' => [],
'prompts' => [],
],
'serverInfo' => [
'name' => 'Naming Server',
'version' => '1.0.0',
],
];
}
/**
* MCPツールのリストを返す
*
* @return array List of tools
*/
public function listMcpTools(): array
{
return [
'tools' => [
[
'name' => 'Naming Tool',
'description' => '本名からあだ名を生成します.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'yourname' => [
'type' => 'string',
'description' => 'あなたの名前.'
]
],
'required' => ['yourname']
],
],
]
];
}
/**
* MCPツールを呼び出す
*
* @param array{
* name: string,
* arguments: array<mixed>,
* } $params Parameters for the tool call
* @return array Response from the tool call
*/
public function callMcpTool(array $params): array
{
if ($params['name'] === 'Naming Tool') {
return [
'content' => [
[
'type' => 'text',
'text' => 'Your name is アドマイヤベガ.'
],
],
'isError' => false,
];
}
// ツールがない場合は例外を投げる
throw new \Exception('Unknown tool: ' . $params['name'], -32602);
}
}
Routes.yaml
ルーティングの設定です。
作成したActionメソッドをURLに紐づけます。
-
name: 'Flow welcome screen'
uriPattern: 'sse'
defaults:
'@package': 'NNHKRNK.MCP'
'@controller': 'MCP'
'@action': 'healthCheck'
httpMethods: ['GET']
-
name: 'Flow welcome screen'
uriPattern: 'sse'
defaults:
'@package': 'NNHKRNK.MCP'
'@controller': 'MCP'
'@action': 'handleMcp'
httpMethods: ['POST']
動作確認
実際に動作確認していきましょう。まずはFlowを起動させます。
$ ./flow server:run
Server running. Please go to http://127.0.0.1:8081 to browse the application.
[Sun Jun 1 12:26:26 2025] PHP 8.2.17 Development Server (http://127.0.0.1:8081) started
続いて、VScodeでMCPサーバを登録します。settings.jsonに以下のように記述しました。
"mcp": {
"inputs": [],
"servers": {
"naming-mcp": {
"type": "sse",
"url": "http://localhost:8081/sse"
}
}
}
設定後に出てくる「start」を押すと、MCPサーバへの初回疎通確認リクエストが飛びます。
疎通確認ができると、以下のような表記に変わります。ツールもうまく認識されてそうです。
最後に、プロンプトから実行できるか試してみます。
あだ名MCPサーバを使って、私「koya」のあだ名をつけてください。
問題なく実行できました!
あだ名の由来はLLMがよしなに考えてくれるんですね、面白い。
終わりに
今回はPHPでMCPサーバを実装してみました。
MCP自体がシンプルで分かりやすいプロトコルなので、実装もスムーズに進み面白かったです。
今後は細部などの実装もやっていこうと思います。
ここまでご覧いただきありがとうございました!