ツールサーバーが実際に送るもの
Model Context Protocol (MCP) は、エージェントが別のプログラムのツールを見つけて呼ぶための、標準の方法です。メッセージは素朴な JSON で、標準入出力に1行ずつ流れます。メッセージが読めれば、どのツールサーバーも調べられます。
単独では動くのに、あなたのエージェントとは動かないツールサーバーは、たいてい、どれか1つのメッセージの形が違っています。
このレッスンのゴール
メモ
ツールサーバーとそのクライアントが交わすメッセージを、読んで書ける。
キーワード
| ことば | いみ | れい |
|---|---|---|
| JSON-RPC | メソッドを呼ぶための、JSON のメッセージ形式 | |
| リクエスト | id を持ち、応答を待つメッセージ | |
| 通知 | id がなく、応答もないメッセージ | |
| stdio | プロセスの標準入力と標準出力 | |
| エラーコード | 失敗の種類を表す数 | -32601 |
学生が入力するセルは、このプレビューでは空白です。
JSON-RPC のメッセージは3種類
リクエストは、id、メソッド名、省略できる引数を持ちます。応答は、その id を写し、結果かエラーのどちらかを持ちます。通知は id を持たないので、何も応答しません。
stdio では、1つのメッセージが JSON の1行です。サーバーは、出力にメッセージだけを書き、ログはエラー出力に送ります。
例: 3つのメッセージ
| 種類 | 例 | 応答 |
|---|---|---|
| リクエスト | {"jsonrpc":"2.0","id":2,"method":"tools/list"} | id が 2 の応答 |
| 応答 | {"jsonrpc":"2.0","id":2,"result":{"tools":[]}} | なし |
| 通知 | {"jsonrpc":"2.0","method":"notifications/initialized"} | なし |
学生が入力するセルは、このプレビューでは空白です。
形であって、プロトコルの全部ではない
重要
下のサーバーは、本物のプロトコルの形を小さくしたものです。本物のサーバーは公式の SDK を使い、バージョン、取り消し、ほかの内容の種類、多くのメソッドも扱います。ここでは何も入れません。
ツールサーバーを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const PRICES = { \"おにぎり\": 150, \"お茶\": 140 };' 'const TOOLS = [{ name: \"get_price\", description: \"店の商品の値段を円で返す\", inputSchema: { type: \"object\", properties: { item: { type: \"string\" } }, required: [\"item\"] } }];' 'function send(message) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, message)) + \"\n\"); }' 'function reply(id, result) { send({ id: id, result: result }); }' 'function fail(id, code, text) { send({ id: id, error: { code: code, message: text } }); }' 'function text(words, isError) { return { content: [{ type: \"text\", text: words }], isError: isError }; }' > server.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ツールサーバーを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function call(id, params) {' ' if (params.name === \"get_price\") {' ' const price = PRICES[(params.arguments || {}).item];' ' if (price === undefined) return reply(id, text(\"不明な商品\", true));' ' return reply(id, text(price + \" 円\", false));' ' }' ' return fail(id, -32602, \"不明なツール: \" + params.name);' '}' 'function handle(msg) {' ' if (msg.id === undefined) return;' ' if (msg.method === \"initialize\") return reply(msg.id, { protocolVersion: \"2024-11-05\", capabilities: { tools: {} }, serverInfo: { name: \"shop\", version: \"0.1\" } });' ' if (msg.method === \"tools/list\") return reply(msg.id, { tools: TOOLS });' ' if (msg.method === \"tools/call\") return call(msg.id, msg.params || {});' ' return fail(msg.id, -32601, \"メソッドが見つかりません: \" + msg.method);' '}' >> server.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ツールサーバーを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) {' ' try { handle(JSON.parse(line)); } catch (error) { fail(null, -32700, \"解析エラー\"); }' '});' >> server.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本付きのクライアントを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const server = require(\"child_process\").spawn(\"node\", [\"server.js\"]);' 'const call = function (n, name, args) { return { id: n, method: \"tools/call\", params: { name: name, arguments: args } }; };' 'const script = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2024-11-05\", capabilities: {}, clientInfo: { name: \"demo\", version: \"0.1\" } } },' ' { method: \"notifications/initialized\" },' ' { id: 2, method: \"tools/list\" },' ' call(3, \"get_price\", { item: \"お茶\" }),' ' call(4, \"get_price\", { item: \"キウイ\" }),' ' call(5, \"delete_all\", {}),' ' { id: 6, method: \"resources/list\" },' '];' > client.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
台本付きのクライアントを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'let waiting = 6;' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' ' console.log(\"<- \" + line);' ' waiting -= 1;' ' if (waiting === 0) server.stdin.end();' '});' 'script.forEach(function (m) {' ' const full = Object.assign({ jsonrpc: \"2.0\" }, m);' ' console.log(\"-> \" + JSON.stringify(full));' ' server.stdin.write(JSON.stringify(full) + \"\n\");' '});' >> client.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
クライアントをサーバーに向けて実行
docker exec -w /home/node/project agent node client.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| リクエスト | 応答 |
|---|---|
| initialize | プロトコルのバージョンとサーバー名を持つ結果 |
| tools/list | get_price を並べた結果 |
| お茶の get_price | 「140 円」の文字列を持つ結果 |
| キウイの get_price | isError が true で、「不明な商品」の結果 |
| delete_all | コード -32602 のエラー |
| resources/list | コード -32601 のエラー |
学生が入力するセルは、このプレビューでは空白です。
セッションには順序がある
クライアントは、まず initialize を送り、サーバーは、できることを答えます。次にクライアントは、準備ができたという通知を送ります。そのあとで初めて、tools/list を頼み、tools/call を使います。
エージェントは、一覧にあるツールをモデルに見せます。モデルがツールを求めたら、クライアントが tools/call を送ります。
失敗には2種類ある
プロトコルのエラーは、JSON-RPC のエラー応答です。リクエスト自体が間違っています。不明なメソッド、不明なツール、読めない JSON などです。それぞれコードがあり、メソッドがないときは -32601 です。
ツールのエラーは、isError が true の普通の結果です。呼び出しは正しかったのに、ツールができなかったのです。モデルは文章を読んで、別の方法を試せます。
よくある間違い
注意
「失敗はすべて JSON-RPC のエラーにすべきだ」と考える人が多くいます。エラー応答は、リクエストの形が間違っていると伝えるものです。動いたのに商品が見つからなかったツールは、isError が true の結果を返します。そうすれば、モデルは理由が分かり、立て直せます。