1. MCP と JSON‑RPC: 一度理解しておけばよい「退屈な」基礎
前回の講義では、そもそも MCP が何のためにあり、Apps SDK のスタックにどう組み込まれるのかを話しました。今回はフォーカスを「最も退屈な層」— MCP メッセージのフォーマット—に絞り、素の JSON ログを自信をもって読み、ChatGPT があなたのサーバーへ何を送っているのか、サーバーが何を返しているのかを理解できるようにします。
MCP はデータのトランスポートとして JSON‑RPC 2.0 を使います。すべてのリクエスト、レスポンス、通知は予測可能なスキーマを持つ通常の JSON オブジェクトです。
つまり「各サービスが独自形式を考える」のではなく、ベースとなる契約があります:
- リクエストには必須フィールド jsonrpc(通常は "2.0")、ユニークな id、メソッド名の文字列 method、そしてパラメータを収めた params オブジェクトがあります。
- レスポンスは id でリクエストと結び付き、result か error のどちらか一方を含みます。
- 通知(notifications)はリクエストに似ていますが id がなく、返信はありません。
見た目はだいたい次のとおりです:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/list",
"params": {
"cursor": null
}
}
これは request です。成功時の応答は次のとおりです:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"tools": [],
"nextCursor": null
}
}
「これは普通の RPC だよね」と思ったなら、そのとおりです。MCP は、どのメソッドが存在するか(tools/list、tools/call、resources/list、prompts/list …)と、どのフォーマットでパラメータやデータをやり取りするかを定めています。
押さえておきたいのは次の点です。JSON‑RPC は「リクエスト–レスポンス–通知」という枠組みです。MCP は「どんなリクエストがあり、その中身がどうなるか」を定義しています。
2. Request: MCP が何かを依頼する方法
まずはリクエストから始めましょう。常に「誰かが何かをしたい」という方向に向かいます。通常はクライアント → サーバー(ChatGPT → あなたの MCP サーバー)ですが、MCP は逆向き(サーバーがクライアントに sampling や elicitation を依頼)のリクエストも許容します。この講義では主に従来のパターン、つまりクライアントがサーバーに依頼するケースを扱います。
どの MCP request にも3つの主要フィールドがあります:
- jsonrpc — JSON‑RPC のバージョン。通常は "2.0"。
- id — リクエスト ID。あらゆる JSON 型を取り得ますが、実際には数値か文字列が多いです。アクティブなリクエスト間で id が一意であることが重要です。
- method — "tools/list" や "tools/call" のような文字列。MCP は許可されるメソッドの集合を規定します。
そして params オブジェクトに、そのメソッド固有のパラメータが入ります。
例: ツール一覧のリクエスト
ChatGPT があなたの MCP サーバーに接続した直後で、呼び出せるツール(tools)を知りたいとします。そのときに送るリクエストは次のようになります:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": null
}
}
cursor フィールドはページネーション用です。ツールが多い場合、サーバーは分割して返せます。
学習用アプリ(ギフト提案)では当面は 1〜2 個のツールで退屈かもしれませんが、プロトコルは同じです。まずは直感的な例として捉えてください。フォーマルな構造は後の tools の節で扱います。
例: ツールの呼び出し(tools/call)
もう少し面白い例です。講義の MCP サーバー回で実装予定の MCP ツール suggest_gifts があるとしましょう。これは次のパラメータを想定しています:
- occasion — 用途・イベント(Birthday、Wedding など);
- budget — ドル建ての数値;
- recipient — 贈る相手の説明文字列。
このツールを使うと決めた ChatGPT は、次のような MCP リクエストを組み立てます:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "birthday",
"budget": 100,
"recipient": "friend who loves board games"
}
}
}
いくつか注意点があります。
第一に、ツール名はサーバー側で宣言したもの(server.registerTool("suggest_gifts", …))が使われます。第二に、arguments オブジェクトは、ツールの説明に添付した JSON Schema に適合する必要があります。
もし GPT がスキーマに従わない引数(例: budget: "100 dollars")を送ろうとした場合、実装次第でプロトコルレベルまたはビジネスロジックレベルのエラーを返して構いません。いまはこのリクエストの全体像を掴むのが目的です。下の tools の節で、同じメッセージをより体系的に見ていきます。
リソースとプロンプト向けのリクエスト
リソースとプロンプトに対するリクエストも同様の見た目です。MCP の仕様は次のメソッドを定めています:
- resources/list — 利用可能なリソースの列挙;
- resources/read(または resources/get)— URI で特定のリソースを読む;
- prompts/list — 利用可能なプロンプト一覧の取得;
- prompts/get — 特定プロンプトのテキストを取得。
ギフトカタログのリソースを読むリクエストの例:
{
"jsonrpc": "2.0",
"id": 15,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
ここでは 2 点だけ覚えておきましょう。第一に、各プリミティブには */list と */get/*/read のメソッドがあります。第二に、メソッド名は常に文字列フィールド method に入り、その他の中身は params オブジェクトに入ります。
3. Reply: MCP の応答 — result と error
レスポンス(reply)は常に id でリクエストと結び付きます。多くの分散システムでの correlationId と同じです。ログを見て、id=7 のリクエストが id=7 のレスポンスを受けていれば、その 1 組だと分かります。
JSON‑RPC は簡単なルールを定めています。レスポンスには result か error のいずれかだけを含め、両方は不可です。MCP はこれを踏まえ、メソッドごとの result の構造(tools/list、tools/call など)と推奨エラーコードを明確にします。
成功応答(result)
suggest_gifts に対する tools/call の成功応答例を見てみましょう。サーバーが処理を終え、適切なギフト候補を見つけ、result にリストで返します:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Here are some gift ideas for your friend..."
}
],
"structuredContent": {
"gifts": [
{ "name": "Board game: Catan", "price": 45 },
{ "name": "Dice set", "price": 20 }
]
},
"isError": false
}
}
ここではいくつか重要なポイントがあります。
- 第一に、content と structuredContent は、Apps SDK で既に見た MCP ツールの応答の主要部分です。モデルは content のテキストを利用し、ウィジェットは structuredContent のデータを綺麗に描画します。
- 第二に、isError フラグはビジネス結果に関するものです。プロトコル的には成功です(JSON は妥当、メソッドは存在、引数も解釈済み)。しかしビジネスロジックとして「ギフトの候補が 1 つも見つからなかった。UX 的にはエラー扱いにする」と決められることがあります。その場合は isError: true を立て、問題を content に説明します。
- 第三に、tools/list、tools/call、*/list、*/get のようなメソッドに対し、MCP の仕様は result に含めるべきフィールドを詳細に定めています。例えば tools/list では、サーバーはツール名・タイトル・説明・入力引数の JSON Schema を含むツール記述の配列を返します。
エラー応答(error)
プロトコルやサーバー側で何か不具合が起きた場合、result の代わりに error オブジェクトを返します。通常は次のフィールドがあります:
- code — 数値のエラーコード;
- message — 人間が読める説明;
- data — オプションの追加データ(スタックトレースや詳細など)。
例: モデルが存在しないメソッドを呼んだ場合:
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: tools/col"
}
}
コード -32601 は JSON‑RPC における典型的な「method not found」です。
ここには重要だが微妙な境界があります。
プロトコルエラー — MCP/JSON‑RPC の規則違反(未知のメソッド、params の型不正、無効な JSON など)。この場合は上位の error を返すのが妥当です。
ビジネスエラー — プロトコルは遵守されているが、業務上の理由で操作が失敗(空のカタログ、特定リソースへの権限なし、ビジネス ID の不正など)。この場合、MCP は通常、妥当な result を返しつつ、isError: true として内容に問題の説明を含めることを推奨します。
この区別は ChatGPT とデバッグツールにとても有益です。ログを見たとき、それが技術的な破綻なのか、ビジネスロジックによる意図的な拒否なのかが一目で分かります。
4. Notifications: 片方向メッセージ
通知(notification)は「返答を期待しない手紙」です。JSON‑RPC では、フィールド id のない通常のリクエストの見た目になります。クライアントは返信してはいけません。
MCP では、ツール/リソース/プロンプトのリスト変更、長時間処理の進捗、ログメッセージなどのイベントに通知が使われます。
もっともよく出会うシンプルな例は、ツール一覧が変更されたという通知です。tools に関する MCP の仕様は capability listChanged と通知 tools/list_changed を定めており、利用可能なツールの集合が変わったらサーバーがこれを送ります。
通知は次のようになります:
{
"jsonrpc": "2.0",
"method": "tools/list_changed",
"params": {
"reason": "New tool 'suggest_gift_cards' was added"
}
}
これに対する応答は不要です。クライアントはこれを受け取ったら「なるほど、tools/list を呼び直してツールのキャッシュを更新しよう」と判断できます。
その他の典型的な MCP 通知(ストリームとイベントのモジュールで詳述します):
- 長時間処理の進捗イベント(notifications/progress);
- サーバーログ(notifications/logging/message);
- リソース(resources/list_changed)やプロンプト(prompts/list_changed)の変更。
ここで重要なのは 1 点だけです。通知 = id なしのリクエスト、かつ返信は不要。ログで id のない JSON を見たら、それはほぼ notification です。
Insight
実験的に、ChatGPT App は送られた MCP 通知(MCP‑notification)を無視することが確認されています。ただし、ChatGPT Apps はまだ発展の初期段階にあり、近い将来 MCP プロトコル全体を完全にサポートする可能性は非常に高いです。したがって、この側面についても学んでおくことをお勧めします。
5. メッセージ内での tools/resources/prompts の姿
ここからが本題です。これまで話してきた tools、resources、prompts が、MCP メッセージ内部で具体的にどう記述されるのかを見ていきます。
Tools: 記述と呼び出し
プロトコルレベルで、tools には 2 つの主要なプロセスがあります:
- discovery — クライアントが利用可能なツールを把握する;
- invocation — クライアントが特定のツールを呼び出す。
上で tools/list と tools/call をちらっと見ました。ここではもう少し体系的に、これらがどのプロセスをカバーし、result に何が返るのかを確認します。
5.1.1. ツール一覧 — tools/list
すでに tools/list のリクエストは見ました。応答の構造を見てみましょう。MCP の仕様では、result.tools に各ツールを記述するオブジェクトの配列を返すべきとされています。各ツールは必ず次を持ちます:
- name — 後で tools/call に使うユニークな名前;
- title — 短いタイトル(人間とモデルの双方が目にします);
- description — そのツールが何をするかの詳しい説明(同僚に説明するイメージ);
- inputSchema — ツール引数の JSON Schema。
私たちの suggest_gifts に対する tools/list の応答は(大幅に簡略化して)次のようになります:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas for a given occasion and budget.",
"inputSchema": {
"type": "object",
"properties": {
"occasion": { "type": "string" },
"budget": { "type": "number" },
"recipient": { "type": "string" }
},
"required": ["occasion", "budget"]
}
}
],
"nextCursor": null
}
}
Apps SDK でツール登録時に inputSchema を書いたことがあるなら、ほぼ同じオブジェクトを「上から」— TypeScript オブジェクトとして—見ています。MCP は単にそれをプロトコルでクライアントに渡しているだけです。
5.1.2. ツール呼び出し — tools/call
呼び出しのフォーマットは既に触れました。MCP の仕様は、params に次が含まれるべきだと述べています:
- name — ツール名;
- arguments — inputSchema に適合するオブジェクト。
例えば:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "wedding",
"budget": 150,
"recipient": "coworker from marketing"
}
}
}
応答では、サーバーは content、structuredContent、そしてオプションで _meta(例えば特定のウィジェットにこのツールを結び付けたい場合の openai/outputTemplate など)を含む result を返します。
この tools/list → tools/call の連携が MCP ツールの基本ループです。まず discovery、その後に利用です。
Resources: アドレス可能なデータ
MCP における Resources は、クライアントが URI で参照できるあらゆるデータ(ファイル、DB レコード、設定、カタログ等)です。
標準的な操作は次のとおりです:
- resources/list — どのリソースがあるかを知る;
- resources/read — 特定のリソース(またはその一部)を読む。
例えば gift_catalog というリソースがあり、ギフトの基本カタログ(カテゴリ、ブランド、最小・最大価格)を記述しているとします。サーバーはその URI を "mcp://gift-server/resources/gift_catalog" として宣言できます。
resources/list への応答は(簡略化して)次のようになります:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "mcp://gift-server/resources/gift_catalog", // 単なる一意な文字列。mcp はプロトコルではない。
"name": "gift_catalog",
"description": "Base catalog of gifts with categories and prices",
"mimeType": "application/json"
}
],
"nextCursor": null
}
}
読み取り — resources/read は次のとおりです:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
応答にはコンテンツ本体とメタデータを含められます:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"contents": [
{
"uri": "mcp://gift-server/resources/gift_catalog",
"mimeType": "application/json",
"text": "{\"categories\":[\"boardgames\",\"books\"]}"
}
]
}
}
要点は単純です。リソースはアドレス可能なデータ、ツールは操作。MCP は両者をプロトコルで明示化します。
Prompts: 再利用可能なテンプレート
Prompts は「用意済みの指示」やテンプレートで、サーバーがクライアントに提供できます。MCP では次の属性を持つプリミティブとして扱います:
- 名前;
- 人間が読めるタイトル/説明;
- 内容(しばしば system プロンプトのテンプレートや few‑shot 例)。
そして予想どおり、メソッドは 2 つです:
- prompts/list — 利用できるプロンプトを知る;
- prompts/get — 単一プロンプトの内容を取得。
例えば、ギフトと一緒に贈るお祝いメッセージを特定の文体で生成したいとします。その場合、MCP サーバーで gift_congrats_style という prompt を宣言できます。
prompts/list の応答は次のようになります:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"prompts": [
{
"name": "gift_congrats_style",
"description": "Style guide for birthday congratulations in a friendly tone"
}
]
}
}
そして prompts/get は、クライアントが LLM に system‑prompt の一部として渡せるテキスト(または構造化コンテンツ)を返します。リクエストと応答の例:
{
"jsonrpc": "2.0",
"id": 11,
"method": "prompts/get",
"params": {
"name": "gift_congrats_style"
}
}
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"prompt": {
"name": "gift_congrats_style",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a friendly assistant that writes short, warm birthday congratulations..."
}
]
}
]
}
}
}
6. Apps SDK とウィジェットとの関係
現時点では MCP‑JSON がやや冗長に見えるかもしれません。ここで、Apps SDK を通じて既に触れていることと結び付けてみましょう。
フロントエンドのウィジェットでは、例えば次のようなコードがあります:
// ChatGPT サンドボックス内の React コンポーネント内
async function fetchGifts() {
const result = await window.openai.callTool("suggest_gifts", {
occasion: "birthday",
budget: 50,
recipient: "friend who loves sci-fi"
});
console.log(result);
}
Apps SDK レベルでは、これは次のことをしてくれる便利な関数です:
- MCP サーバーの URL を知っている(アプリ構成から)。
- suggest_gifts という名前からツールの記述を見付けられる。
- あなたの呼び出しを MCP リクエスト tools/call に梱包する。
- 選択されたトランスポート(HTTP/SSE)で送る。
- MCP リプライを待ち、result を展開して JavaScript の result として返す。
これを図にするとだいたい次のようになります:
sequenceDiagram
participant Widget
participant AppsSDK as Apps SDK
participant MCP as MCP server
Widget->>AppsSDK: window.openai.callTool("suggest_gifts", {...})
AppsSDK->>MCP: JSON { id:7, method:"tools/call", params:{...} }
MCP-->>AppsSDK: JSON { id:7, result:{ content, structuredContent } }
AppsSDK-->>Widget: result (ToolOutput)
Widget->>Widget: setState(toolOutput)
MCP フォーマットを理解すると、2 つの優れたスキルが身に付きます。
第一に、素の MCP ログ(例えば MCP Inspector。これについては別講義)をちゃんと見て、どんな tools/call が飛び、引数はどうで、result か error に何が返ってきたのかを理解できるようになります。
第二に、ツールやリソースを設計する際、TypeScript の型だけでなく、MCP スキーマという観点でも考えられるようになります。これが JSON ではどう見えるか、他のクライアント(あなたの MCP サーバーに接続できるエージェントなど)にとってどれくらい扱いやすいか、といった観点です。
7. ミニ演習: MCP‑JSON を読み、そして「直す」
MCP フォーマットに慣れるには、いくつかのメッセージを手作業で分解してみるのが一番です。ここでは tools/list → tools/call → 結果、という一連の対話を例にします。
クライアントがツール一覧を欲しがる
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
ここから分かること:
- これは request(id がある)。
- メソッドは tools/list、つまりツールの discovery の話。
- パラメータは空で、ページネーションなし。
サーバーは次のように応答します:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas",
"inputSchema": { "type": "object", "properties": { "occasion": { "type": "string" } } }
}
]
}
}
同じ id: 1 なので、まさにそのリクエストへの応答だとすぐ分かります。プロトコルは成功(result があり、error はない)で、クライアントは suggest_gifts というツールが存在することを知りました。
クライアントがツールを呼び出す
次にクライアントは tools/call を行います:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "anniversary"
}
}
}
もしサーバーが budget も必須としているのに、モデルが指定しなかった場合、サーバーは次のどちらかができます:
- プロトコルエラーを返す(例えば「invalid params」を示す error)。
- デフォルト判断をする(例えば平均的な予算を使う)うえで通常の result を返す。
前述の用語でいえば、前者はプロトコルエラー(上位の error)、後者はビジネスロジックの領域です。妥当な result を返した上で、それをビジネスエラー(isError: true)とみなすか通常動作とみなすかを決めます。
引数エラーの場合の応答例:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Missing required property 'budget' in arguments"
}
}
繰り返しになりますが、これはビジネスエラーとは区別しましょう。プロトコルが破られている(引数がスキーマに適合していない)ので、ここでは error が適切です。
壊れた例: バグを探す
初心者がたまに書いてしまう JSON がこちらです:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"tool": "suggest_gifts",
"args": {
"occasion": "birthday",
"budget": 100
}
}
}
一見もっともらしく見えますが、MCP の仕様に照らすと tool と args は、期待される name と arguments に一致していないことに気付きます。
MCP SDK のクライアント/サーバーは、おそらくこんな JSON を生成しませんが、仕様を知らずに手作業で統合していると、こうしたバグは現実に起こり得ます。だからこそ、このコースでは SDK のラッパーだけでなく、プロトコルを「素のまま」扱っているのです。
8. MCP メッセージでよくあるミス
ミス #1: プロトコルエラーとビジネスエラーの混同。
開発者は慣習的に「うまくいかなかったもの」をすべて上位の error に包みがちです—リソース欠如も、不正な引数も、DB 障害も。MCP の文脈では区別が有益です。JSON 構造や呼び出しスキーマが破られている(メソッド違い、フィールド違い、型違い)なら error を返します。一方、ツールが業務上の理由で操作を実行できなかった(その予算ではギフトが見つからない、ユーザーが見つからない)なら、妥当な result を返し、isError: true と content に分かりやすいメッセージを含める方が良いです。そうすることで、ChatGPT モデルもデバッガも「通信経路が壊れた」のか「サーバーが意図的に拒否した」のかを適切に区別できます。
ミス #2: id とリクエスト相関の無視。
MCP サーバーのログで、手書きの出力に id がなかったり、異なるアクティブなリクエストで id が重複していたりすることがあります。単純な Hello World なら誤魔化せても、並列呼び出しやリトライが入ると、どの応答がどのリクエストに対応するのか分からなくなります。JSON‑RPC は、リクエストの生存期間に一意な id を要求しており、MCP はこの規則に依存します。公式 SDK を使っているなら id を意識する必要はありませんが、独自のトランスポートやロギングを書く場合は、id を保持し、出力するのを忘れないでください。これは不可解なバグをデバッグするときに最初に頼る情報です。
ミス #3: 同一メソッドの result 構造が不安定。
状況に応じて応答フォーマットを「ちょっとだけ」変えたくなることがあります。ギフトを配列で返したり、1 行の文字列だけのオブジェクトにしたり、structuredContent なしで text だけにしたり。モデルは何とか処理できるかもしれませんが、あなたのウィジェットや他の MCP クライアントはまず対応できません。各メソッドの result 構造は MCP の仕様で予測可能に定められています。これを守りましょう。別のフォーマットが必要なら、別のツールやバージョンとして宣言し、スキーマをその場で変えないことを勧めます。
ミス #4: params の過不足。
カスタム実装でありがちな問題は、params に MCP が想定していないものを足したり、必須フィールドを欠かしたりすることです。例えば tools/call で name の代わりに toolName を送ったり、resources/read で uri の代わりに resourceId を送ったり。MCP SDK は通常この手の問題を検証し、分かりやすい例外を投げますが、プロトコル寄りで作業していると「サーバーが自分を理解してくれない理由」を長時間追う羽目になります。良いコツは、ハンドラの近くに仕様や実動ログからの正しい JSON リクエスト例を置いておき、自分が送っているものと比較することです。
ミス #5: 通知を「第二の応答チャネル」として使う。
notifications を見つけた途端、通常の replies の代わりに通知で結果を送ろうとする開発者がいます。「MCP だし SSE もあるし、全部通知でプッシュしよう」と。しかし、JSON‑RPC の通知は定義上、特定の id に紐付かず、クライアントにもリクエストへの応答として認識されません。その結果、デバッグが難しくなり、どのツール呼び出しに対応するメッセージかが分からなくなります。通知はイベント(tools/resources/prompts のリスト変更、新しい進捗、ログの到着)には最適ですが、tools/call など通常の応答には使うべきではありません。
ミス #6: MCP のログやインスペクタを見ない。
最も「人間らしい」ミスは、ChatGPT の UI だけで統合をデバッグしようとすることです。「ボタンを押した、何か来ない、いつか調べよう」。素の MCP メッセージ(requests、replies、notifications)を見ない限り、問題がどのレベルにあるのか(モデルがツールを呼ばなかったのか、Apps SDK が MCP サーバーまで届かなかったのか、サーバーが間違った JSON を返したのか、あるいはウィジェット描画で壊れたのか)を理解するのは難しいです。MCP Inspector / Jam と MCP メッセージの構造化ロギングは最良の友です。一度でも生の tools/call と tools/list をログで見れば、MCP メッセージフォーマットは「魔法」ではなく、日常的なエンジニアリング作業に過ぎないと感じられるようになります。
GO TO FULL VERSION