MCPが定めること — 三種類のメッセージとツールの契約
一言でいうと
MCP(Model Context Protocol)は、LLMアプリケーションと外部のツール・データをつなぐオープンなプロトコルです。メッセージはJSON-RPC 2.0で、接続は状態を持ち、双方がケーパビリティをネゴシエーションします。この3つの文が、仕様の概要が「Key Details」として書いているすべてで、残りはその上に積み重ねた約束事です。
なぜ必要なのか
エージェントを1つ作ると、ツールがつながります。データベースを読むツール、ファイルを開くツール、社内APIを呼ぶツールです。最初は、そのツールを関数3つで作ってモデルに説明を付ければ終わりです。問題は、2つ目のエージェントアプリを作るときと、ほかのチームのツールを借りて使うときに起きます。ツールの名前・引数・結果の形式・エラー処理・承認手順をアプリごとに決め直すことになり、ツール側もアプリごとに異なる接続コードを持つことになります。N個のアプリとM個のツールがあれば、N×M個のアダプターが生まれます。
仕様はこの問題を、Language Server Protocol(LSP)にたとえています。エディターごとに言語サポートを別々に作っていた時代を、LSPが「エディター ↔ 言語サーバー」という1つの規約で終わらせたように、MCPは「LLMアプリ ↔ ツールサーバー」を1つの規約で終わらせようとしています。そのため、仕様が定義するのはモデルの動作ではなく、メッセージの形です。誰が先に話し、何を返さなければならず、失敗をどう知らせるのかを定めています。
役割は3つです。ホスト(Host)は接続を開始するLLMアプリで、クライアントはそのホストの中でサーバー1つと1:1でつながるコネクターで、サーバーは文脈とケーパビリティを提供する側です。サーバーが提供できるものは3つあります。リソース(読み取るデータ)、プロンプト(テンプレート)、そしてこのコースの主題であるツール(モデルが実行する関数)です。
どう動くのか
メッセージは3種類あります。基本プロトコルは、リクエスト・レスポンス・通知を定義しています。リクエストはidとmethodを持ち、レスポンスは同じidにresultまたはerrorのどちらか1つだけを載せます。通知にはidがなく、受け取った側は返答してはいけません(MUST NOT)。JSON-RPC 2.0の原文はidにnullを許しますが、MCPはさらに一歩進めてnullを禁止し、1つのセッションの中で同じidを再利用することも禁止しています。
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "count_orders", "arguments": {"status": "paid"}}}
接続には順序があります。ライフサイクルは、初期化 → 運用 → 終了の3段階を定めています。クライアントがinitializeリクエストに、自分がサポートするプロトコルのバージョン・ケーパビリティ・実装情報を載せて送ると、サーバーは自分のバージョンとケーパビリティ(tools、resources、prompts、loggingなど)とserverInfoで返答します。サーバーがそのバージョンをサポートしていれば同じバージョンを、そうでなければ自分が知っている別のバージョンを返し、クライアントはそのバージョンを知らなければ接続を切らなければなりません(SHOULD)。そのあとクライアントがnotifications/initialized通知を送って、はじめて運用段階になります。初期化の前は、双方ともpingのほかにはリクエストを送らないのがルールです。
ツールは2つのメソッドで完結します。ツール仕様のtools/listはツールの一覧を返し、各ツールはname・description・inputSchema(JSON Schema)を持ちます。モデルはこのスキーマを読んで引数を作ります。tools/callはnameとargumentsを受け取り、content配列(テキスト・画像・音声・リソースリンク)を返します。ここで重要な区別が1つあります。エラーが2種類あることです。知らないツール名や不正な引数のように、リクエストそのものが間違っている場合は、JSON-RPCのerror(仕様の例は-32602)で返します。ツールが実行されている途中で失敗した場合(APIの失敗、ビジネスルール違反)は、resultの中にisError: trueと説明のテキストを入れて返します。後者は、モデルが読んで再試行できなければならないからです。
トランスポートは2つあります。トランスポート仕様のstdioは、クライアントがサーバーを子プロセスとして起動し、stdinにリクエストを書き込んでstdoutからレスポンスを読む方式です。メッセージは改行で区切られ、メッセージの中に改行があってはならず、サーバーはstdoutにMCPメッセージ以外を書いてはならず(MUST NOT)、ログはstderrに書けます。Streamable HTTPは、1つのエンドポイントがPOSTとGETを受け付け、必要に応じてSSEで複数のメッセージを流す方式で、ローカルで動かすときはOriginヘッダーを検証し、127.0.0.1だけにバインドするよう明記しています。ブラウザー上のWebページがDNSリバインディングで、手元のローカルMCPサーバーを操作してしまうのを防ぐためです。
現場での姿
このコースの題名にある事故は、次のようにして起きます。誰かが社内DBをラップしたMCPサーバーを作り、ツールはrun_sql1つだけでした。エージェントが「テストデータを整理して」と頼まれてDROP TABLE ordersを作って送り、サーバーはそれを実行しました。モデルが悪いのではありません。仕様はツールをモデルが選ぶもの(model-controlled)と定義し、そのため人が拒否できる場所が常に必要であり(SHOULD)、サーバーはすべての入力を検証し、アクセス制御を実装し、呼び出し頻度を制限しなければならない(MUST)と書いています。その文章がコードのどこにもなかったことが原因です。
現場でMCPサーバーを見るとき、まず確認することは3つあります。tools/listが出すツールがどれだけ狭いか(万能ツールがあれば事故の予約です)、破壊的なツールに確認手順があるか、そしてstdoutを汚さないこと、つまりデバッグ用のprintが1行あるだけでクライアントのJSONパーサーが壊れ、「サーバーが応答しない」ように見える事故が実際に頻繁に起きます。このコースの3つのラボが、その3つを順番に扱います。
次のクイズで確認すること
メッセージ3種類の区別(とくに通知に返答してはいけない理由)、initializeの順序とバージョンのネゴシエーション、2種類のエラーの区別、stdioにおけるstdoutとstderrの役割を問います。すべてこの文章に書いてある内容です。