どうしてもCursorに今の時間と天気を教えてあげたいんだ!
Date: 2025/05/12 13:46
Category: Protocol実装, 車輪の再発明
はじめに
生成AIとおしゃべりしていると、「あれ、こいつ時間わかってないな?」と思う瞬間がたまにある。
最近のChatGPTはちゃんと時間感覚が身についてきたけど、Cursorはまだだった。
でも──
ついに、Cursorも「今の天気」と「現在の時刻」をわかるようになった。
これは、Cursorに時間と天気を理解させるまでの、私とMCPとの泥臭い格闘の記録である。
MCPを知る、MCPを試したくなる
2025年3月ごろから、ModelContextProtocol(MCP)が急に注目されはじめた。
Zennなどの技術ブログでも取り上げられることが増え、
「生成AIが外部のツールを使えるようになるらしい」という話題が盛り上がっていた。
ちょうどその頃、「じゃあ試してみるか」と思ったのが、すべての始まりだった。
Model Context Protocolって?
Model Context Protocol(MCP)は、
Claude を提供する Anthropic 社が提案した、
生成AIがさまざまなツールやシステムと連携するためのプロトコルである。
よく「AIにとってのUSB端子」と例えられることが多い。
つまり——
生成AIに道具を使わせたければ、このMCPという決まりごとに従ってツールを定義すればよい、というわけだ。
しかも、仕様を調べてみると、いろんな通信方法、実装方法はあれど、結局は JSON-RPC2.0という規格に則り、決められたフォーマットの JSONをやりとりしているだけというだいぶシンプルな仕様だった。
……と思ったんだけど、実際にやってみたら思ったよりややこしくて、
「MCPが難しいんじゃない、JSONが難しいんだ」 という境地にたどり着いた。
MCPの最小構成はこれだけ
MCPサーバとして認識してもらうために必要なのは、基本的にこの3つのメッセージに対応するだけ。
これをMCPクライアント(AIエージェント)が、こんにちはーって投げてくる。
1{ 2 "jsonrpc": "2.0", 3 "id": 1, 4 "method": "initialize", 5 "params": { 6 "protocolVersion": "2024-11-05", 7 "clientInfo": {"name": "example-client", "version": "1.0.0"} 8 } 9} 10
なので、MCPサーバは、以下のようなレスポンスで何ができるのかなどを自己紹介する必要がある。これができないとAIエージェント側にMCPサーバとして認識してもらえない。
1{ 2 "jsonrpc": "2.0", 3 "id": 1, 4 "result": { 5 "protocolVersion": "2024-11-05", 6 "capabilities": { 7 "tools": { 8 "listChanged": false 9 } 10 }, 11 "serverInfo": { 12 "name": "mcp-tools-php", 13 "version": "0.0.1" 14 } 15 } 16} 17
AIエージェントは、「このMCPサーバにはどんなツールがあるんですか?」って以下のようなメッセージで聞いてくるので、
1{ 2 "jsonrpc": "2.0", 3 "id": 2, 4 "method": "tools/list" 5} 6
このリクエストに対して、MCPサーバは以下のようにツール一覧を返す。
1{ 2 "jsonrpc": "2.0", 3 "id": 2, 4 "result": { 5 "tools": [ 6 { 7 "name": "weather", 8 "description": "都市の天気を取得します", 9 "inputSchema": { 10 "type": "object", 11 "properties": { 12 "location": { 13 "type": "string", 14 "description": "都市名 (例: Tokyo,JP)" 15 } 16 }, 17 "required": ["location"] 18 } 19 }, 20 { 21 "name": "clock", 22 "description": "現在の時刻を取得します", 23 "inputSchema": { 24 "type": "object", 25 "properties": { 26 "timezone": { 27 "type": "string", 28 "description": "タイムゾーン (例: Asia/Tokyo)" 29 } 30 }, 31 "required": ["timezone"] 32 } 33 } 34 ] 35 } 36} 37
この形式で返すことで、AIエージェントは「どんな道具が使えるか」「どういう引数が必要か」を理解することができる。
で、実際に道具を使う場合は、以下のようなリクエストを投げてくるので、
1{ 2 "jsonrpc": "2.0", 3 "id": 3, 4 "method": "tools/call", 5 "params": { 6 "name": "clock", 7 "arguments": {"timezone": "Asia/Tokyo"} 8 } 9} 10
以下のようにレスポンス結果に、ツールの実行結果を載せて返してあげる、ただそれだけである。
1{ 2 "jsonrpc": "2.0", 3 "id": 3, 4 "result": { 5 "content": [ 6 { 7 "type": "text", 8 "text": "2025-05-11 01:23:45" 9 } 10 ] 11 } 12} 13
このように result.content[].text
の中に、文字列としてツールの出力(JSONや自然文)を入れて返すのが定石である。
CopilotやCursorはこの構造を読み取り、「道具からの応答」としてユーザーに自然な形で表示してくれる。
この3つを受け取って、それぞれ正しくJSON-RPCのレスポンスを返すだけで、
CursorやGitHub Copilot、Claudeなどが自作ツールを「道具」として使ってくれるようになる。
形式はシンプルだけど、ミスすると静かに失敗するので注意が必要だ。例えば、JSONの構造が微妙に違うだけで、AI側がツールの応答を正しく解釈できず、結果的にツールが動作していないように見えてしまうことがある。
MCPをブログサーバに実装してみる
最初にやったのは、このブログサーバに実験的に MCP サーバを実装して、Cursorに認識させるのを目指すということだった。
結論を先に述べると、2025年5月12日現在、Cursor はこのMCPサーバを認知することはできない。
一応、以下のように MCP Inspectorという MCPをテストできるサーバではちゃんと動作することを確認できたので、嘘はついてない、ちゃんと完成してるし動いている。
のだが、このブログサーバのMCPサーバの道具たちは、Cursorに使ってもらうことはできなかった。
最初は、MCPサーバはSSE(Server-Sent Events)で実装しなければならない、という認識のもと、SSEベースでのMCPサーバを組んだ。SSEは、HTTP接続を張ったままサーバーからクライアントに向けて一方向にメッセージを送る仕組みで、リアルタイムに文字を送信するチャットアプリなどでも広く使われている。MCPでも推奨される通信形式のひとつである。
ところが、Netlifyのようなサーバレス環境ではSSEはうまく機能しない。長時間の接続を維持できなかったり、ヘッダーの送信タイミングが制御できなかったりと、制約が多く、結果としてCursorからツールとして認識されることはなかった。また、CursorのMCPクライアント側にも、接続が切断されたあと initialize
を再送しないという問題がフォーラムでも指摘されており、再接続後のやりとりが成立しない場合がある(参考: https://forum.cursor.com/t/mcp-sse-is-not-sending-initialize-message-if-ever-disconnected/77270)。
次に、SSEとNetlifyの相性問題を回避するために、通常のHTTP通信を拡張したステートレスなstream方式 application/json+stream
形式に切り替えた。これは、1つのレスポンス内で複数のJSONオブジェクトを改行区切りで逐次送る形式で、MCP Inspectorなどのツールでは期待通り動作した。
しかしながら、Cursor側のMCPクライアントがこの application/json+stream
形式にまだ対応しておらず、やはりCursorからツールを呼び出すことはできなかった。
要するに、MCPの仕様自体はJSON-RPCとしてはとてもシンプルなのだが、HTTPベースでストリーミングを行おうとした途端に、インフラやクライアント側の制約が一気に顔を出してくる。難しいのはMCPではなく、SSEとJSONと、その環境への適用なのだった。
MCPサーバをローカルで建てる方式を試す
なるほど、つまりSSE も streamable-http での通信もだめかー、私は自分の Cursor に天気も時間も教えてあげられないのかー。
で終わるはずもなく、次の一手を考えた。
GitHub で公開されている MCP サーバのほとんどがローカルで建てる方式だったことを思い出した。HTTPのやりとりが絡むから厄介なのであって、本質である JSON-RPCの純粋なやりとりだけにすればできそうだなと考えた。
で、出来上がったものがこれである。
Cursor や GitHub Copilot、試してないけど Claude の定義ファイルに以下を書いていただいて、OpenWeatherAPIのトークンを環境変数にセットしていただければ、すぐさま現在の天気と日時を把握できるようになる。
1{ 2 "mcpServers": { 3 "mcp-tools-php": { 4 "command": "env", 5 "args": [ 6 "docker", 7 "run", 8 "-i", 9 "--rm", 10 "-e", 11 "OPENWEATHER_API_KEY", 12 "mcp-tools-php" 13 ] 14 } 15 } 16} 17
で、終わってもいいのだが、これも一筋縄じゃいかなかった。
Cursor に教えてもらいながら、Dockerfile, Makefile, mcp.json と次から次へと作っていき、この調子ならすぐ動くところまで行けそうだなと思ったのだが、思ったよりも時間を溶かしてしまった。
Cursor側が MCPサーバを認識して、どんなツールが使えるかを知るというところまでは、爆速で進んだのだが、
ここから割と長い事、このエラーを格闘することになる。
というのも、MCP自体提唱されてから半年程度の新しい概念で、ChatGPTやそれを元にしているAIエージェントも、あまり知らない概念なので、平気で誤った改善案を出してくる。しかもこっちもよく分かってないでやっているので、それに騙されて全然先に進まない。。。
公式ドキュメントのMCPの仕様にたどり着き、公式ドキュメントのリンクを提示したことで、途端に精度が向上し、沼からの脱出となった。
Cursorが、MCPサーバからはエラーが返ってくるけど、なんかローカルにCLIがあったから、それ実行してみますわー、実行できましたわー、の図。
意図せず Cursor に日時を教えることができた瞬間である。README.md にサンプルコマンドを書いておいて良かった。伏線回収になった。
結局、tools/callのレスポンスのJSON-RPCの形式が微妙に間違っていたのが原因だった。
ダメだったレスポンス例1
1{"jsonrpc":"2.0","id":5,"result":{"timezone":"Asia\/Tokyo","now":"2025-05-10 23:33:05"}} 2
ダメだったレスポンス例2
1{"jsonrpc":"2.0","id":7,"result":{"content":[{"timezone":"Asia\/Tokyo","now":"2025-05-10 23:47:09"}]}} 2
うまくいったレスポンス例
1{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"timezone\":\"Asia\\\/Tokyo\",\"now\":\"2025-05-10 23:55:16\"}"}]}} 2
おわかりいただけただろうか、resultの中身が、contentがない、中身がtype, textの形式になっていないなど微妙に異なっていたのだ。
これにより、ようやく MCP Inspector も正しく動作することとなった。
実はここが間違っていても、History を見れば Response がわかるので、一見すると成功しているように見えるのだが、
本当に成功すると、「Run Tool」ボタンの下に、以下のように Tool Result: Success の文字とともにレスポンスが出てくるのである。
これになかなか気づけなかったので、 MCP Inspector ではちゃんと動いているのに、Cursorではエラーになるという謎の現象に出くわしたのだった。
1Tool Result: Success 22025-05-13 01:02:37 3
で、ようやく、Cursor が、clock と weather という2つのツールを使って、
日時と天気を知ることができたのだった。
まとめ
Cursor に 今の時間と天気を教えたい!という一心で、MCPサーバ構築を試みた。
HTTPベースの MCPサーバは、インフラの問題や Cursorがそもそも対応していないなどの問題により、断念した。
GitHubなどの他のMCPサーバは、Stdio(標準入出力)形式で実現しているものが多かったので、JSON-RCP2.0形式でメッセージを返すPHPのサーバが動くDockerイメージを作り対応した。
最終的に、Stdio形式で Cursor に天気と日時を教えることに成功したが、レスポンスの要求するJSON形式を分かっていなかったのもあり、思っていたよりも時間がかかってしまった。
公式ドキュメントを与えて、サンプルのリクエストとレスポンスを吐かせて、ようやく動くものができた。MCPとその周辺技術を学べたのでそれはそれで良かった。
AIが道具を使うには、こうした人間側の準備と理解が不可欠であることを改めて実感した。
Model Context Protocol チョットワカル って言ってもいいんじゃないだろうか?
まさき。です。PHPエンジニアをやってます。
自分の課題を技術で乗り越えるの好きかもしれないです。
フロントエンドは苦手ですが、少しでもできるようになれたらな、ということでNextJSでこのブログサイトを作りました。