CodeGym /コース /ChatGPT Apps /Handshake と capabilities: クライアントはサーバーの機能をどう把握するか

Handshake と capabilities: クライアントはサーバーの機能をどう把握するか

ChatGPT Apps
レベル 6 , レッスン 2
使用可能

1. そもそも handshake はなぜ必要か

REST のエンドポイントが URL を叩ける「別々のドア」の集合だとすれば、MCP はむしろ単一チャネルでの継続的な対話に近いものです。クライアントは単にバラバラのリクエストを送るのではなく、まずセッションを確立します。Handshake はそのセッション冒頭の「顔合わせ」の瞬間です。

MCP ではこの瞬間が、トランスポート(STDIO、HTTP/stream、WebSocket など何でも)確立直後にクライアントが送る特別なリクエスト initialize として実装されています。リクエスト内でクライアントは「自分はこのバージョンの MCP を話し、こういうことができ、こういうクライアントである」と名乗ります。サーバーはそれに対し「自分はこのバージョンとこれらの機能をサポートしている。よろしく」と返します。

正常にやり取りが済むと、クライアントは notifications/initialized を送信し、その後で初めて実務フェーズが始まります: tools/listresources/listtools/call などの有用な呼び出しです。

たとえ話をすると、MCP の handshake は、データセンターにサーバーを搬入する前の賃貸契約のようなものです。ルール(プロトコルの形式、データセンターが提供するサービス、課金の対象など)に合意できていないのにサーバーを運び込んでも意味がありません。

実務的な観点では、handshake は主に次の3つを担います:

  1. プロトコルのバージョン互換性を確認する。
  2. MCP の「プリミティブ」をサーバーがどれだけサポートするかを宣言する: tools、resources、prompts、ロギング、通知など。
  3. クライアントとサーバーのメタ情報(実装の名前とバージョン)を伝える。

2. MCP 接続のライフサイクル: handshake はどこに位置づくか

抽象的な話にしないため、典型的な接続のシナリオ(flow)をかなり簡略化して見てみましょう:

sequenceDiagram
    participant C as クライアント (ChatGPT/Inspector)
    participant S as MCP サーバー

    C->>S: (1) トランスポートを確立 (STDIO/HTTP-stream)
    C->>S: (2) Request: "initialize"
    S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Request: "tools/list" / "resources/list"
    S-->>C: (6) Result: ツール/リソースの一覧
    C->>S: (7) Request: "tools/call" など

技術的には各ステップは次のようになります:

  1. トランスポートが確立される。たとえば ChatGPT があなたのサーバーをサブプロセスとして起動し STDIO に接続する、あるいは Inspector が /mcp に HTTP/stream リクエストを行う、など。
  2. クライアントが JSON-RPC リクエスト initialize を送る。
  3. サーバーは protocolVersioncapabilitiesserverInfo を含む JSON-RPC の結果で応答する。
  4. クライアントが notification notifications/initialized を送る — 「内容を読み込んだので開始できる」というシグナル。
  5. クライアントはサーバーの capabilities を見て、必要に応じて discovery メソッド(tools/listresources/listprompts/list)を呼ぶ。
  6. サーバーはツール/リソース/プロンプトのメタデータを返す。
  7. 以降は「実務」のリクエスト、すなわち tools/callresources/read などが続く。

重要なのは、handshake は単なる通常の JSON-RPC 呼び出し initialize に過ぎないという点です。魔法ではありません。MCP メッセージ形式の講義で既にこの種のリクエストの読み方は学びましたが、唯一の違いは、このメソッドが必ず最初に、一度だけ、特別扱いで実行されるということです。

3. initialize でクライアントが送るもの

initialize リクエストを項目ごとに分解してみます。講義用に簡略化した最小例は次のような形になります:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

この例は MCP 公式ドキュメントに近いものです。params の主要フィールドは次のとおりです:

protocolVersion

MCP 仕様のバージョンを表す文字列で、多くは日付形式です。たとえば "2025-06-18"。これはアプリ自身のバージョンではなく、プロトコルそのもののバージョンです。クライアントは「このバージョンの MCP を話すつもりだ」と宣言します。サーバーは対応していればそれを確認し、そうでなければ未知のバージョンとしてエラーを返すべきです。

これは「クライアントはこう思っているが、サーバーは別のものを実装している」という事態を防ぎます。共通のバージョンが見つからない場合は、互換性のないメッセージをやり取りするより、接続を正直に切るほうが良いのです。

capabilities(クライアント)

クライアントが自分でサポートする MCP の機能を宣言するオブジェクトです。たとえば ChatGPT クライアントはしばしば elicitation キーを指定し、ユーザーへの問い合わせ(追加入力、確認など)を処理できることを示します。

例:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

サーバーはこの情報を使って、プロトコルの拡張機能のうち何が有効かを判断できます。たとえば elicitation があれば、クライアント(ChatGPT)がユーザーに追加質問を投げたり追加データを求めたりできるという意味になります。

clientInfo

単純なメタ情報で、クライアントの名称とバージョンです。

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

サーバー開発者の視点では、ログの宝物です。どのクライアントが今つながっているのか(ChatGPT、MCP Inspector、自作のテストクライアントなど)と、そのバージョンをすぐ確認できます。

4. サーバーが返すもの: initialize result

initialize への応答は、同じ id を持つ通常の JSON-RPC 結果で、result フィールドにサーバーができることの説明が入ります。

リクエスト側ではクライアントの capabilities(クライアント自身がサポートするもの)を見ました。今度は回答側の鏡像であるサーバーの capabilities、つまりサーバーが何をできるかを分解します。概略は次のとおりです:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

同様の構造は、プロトコルの公式説明や SDK の説明にも出てきます。主要な部品は以下のとおりです。

応答の protocolVersion

サーバーはクライアントが提案したバージョンを繰り返すか、(理論上は)複数ある場合に共通の別バージョンを選ぶこともあり得ます。典型的な実装では、サーバーが対応していればクライアントのバージョンをそのまま確認します。対応していなければエラーを返し、通信を終了するべきです。

serverInfo

サーバーのメタ情報(名称、バージョン)です。

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

地味に見えますが、ログでの切り分け・検索に非常に役立ちます。「なぜ ChatGPT のバージョン X が、我々のサーバーのバージョン Y と握手できないのか」といった調査の手掛かりになります。

サーバーの capabilities

ここが最も面白いフィールドです。サーバーがどの MCP プリミティブや拡張をサポートするか、つまり tools/*resources/*prompts/* を処理できるか、リストの変更通知を送れるか、などを宣言します。

もし capabilitiestools セクションがなければ、正しく実装されたクライアントは tools/listtools/call を呼びません。同様に resources がなければ、クライアントは resources/listresources/read を送らないはずです。

このように capabilities は、「このサーバーに対して何が可能で、何が不可能か」を示す軽量な契約です。

5. Capabilities は「スーパーパワーの一覧」のようなもの

以降はサーバー側の capabilities に注目します — これは initialize 応答で返ってくるオブジェクトで、このサーバーがどの MCP プリミティブをサポートするかを決定づけます。

その構造をもう少し詳しく見てみましょう。例(仕様に近いが簡略化):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

このような例は MCP の公式アーキテクチャでも解説されています。セクションごとに読み解きます。

Capabilities.tools

tools キーが存在することは、サーバーが tools/listtools/call に応答できることを意味します。 さらに listChanged: true フラグがあれば、将来的にツールのセットが変わったときに tools/list_changed 通知を送る可能性があることを意味します。

これは ChatGPT にとって有用です。ツール一覧をキャッシュしておき、list_changed を受け取ったらフルの再接続なしに更新できます。

Capabilities.resources

resources セクションは、サーバーがリソース操作(resources/listresources/read、場合によっては検索)をサポートすることを示します。内部のフラグは次のとおりです:

  • subscribe: true — クライアントはリソースの変更に購読できます(ライブログやファイル更新など)。
  • listChanged: true — リソースの追加・削除があった場合、サーバーは resources/list_changed 通知を送れます。

これは巨大なディレクトリや、常に変化する「生きた」データに特に重要です。

Capabilities.prompts

サーバーがあらかじめ用意したプロンプト(たとえばドメインに合わせたテンプレート)を登録する場合、capabilitiesprompts キーが現れます。ここにも listChanged フラグがあることがあります。

クライアントはこのセクションを見て、prompts/list や場合によっては prompts/get が利用できることを理解します。

Capabilities.logging など

一部のサーバー実装は logging も宣言します。これは、MCP 経由で構造化ログをクライアントに送れる(デバッグに便利)ことを意味します。

そのほか(sampling や特定用途の拡張など)も追加されることがあります。プロトコルは拡張可能として設計されており、capabilities に新しいキーを追加しても、古いクライアントは知らないキーを単に無視します。

インサイト

実験的に、ChatGPT App は送信された listChanged メッセージを無視することがわかっています。現時点ではアプリ実装で、最初に宣言したツールのセットをあとから増減させることはできません。MCP プロトコル自体は可能でも、です。

この講座執筆時点の状況: ChatGPT Store への登録時に、ChatGPT はあなたのアプリからツールとリソースの一覧を取得し、それを恒久的にキャッシュします。2026 年のうちに状況が変わる可能性は高いものの、2026 年第 1 四半期内に変わる可能性は低いと言えます。

6. handshake 後の discovery: ツールとリソース一覧の取得方法

Handshake は「サーバーが何をできるのか」に答えます。 次のステップである discovery では、具体的なメソッドを通じて詳細(実際にどんなツールがあるのか、どのリソースが利用可能か、どんなプロンプトが組み込まれているか)を引き出します。

これには discovery メソッド、すなわち tools/listresources/listprompts/list を使います。MCP アーキテクチャのドキュメントでも、handshake → discovery → ツール呼び出し、という流れで説明されています。

リクエスト例 tools/list:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

サーバーの応答には、ツールの配列(名前、説明、引数の JSON Schema、場合によってはカテゴリやアイコンなどのメタデータ)が含まれます。

その後 ChatGPT(や他のクライアント)は一覧をキャッシュし、対話中にこれを使って次のようなことを行います:

  • ユーザーの課題に適したツールを選ぶ。
  • ツール名が存在するかを確認する。
  • tools/call を送る前に引数を検証する。

リソースも同様ですが、resources/list は大量データを一度に持ってこないようにカーソルによるページネーションをサポートすることが多いです。これも MCP 仕様で典型ケースとして扱われています。

7. 私たちのアプリ GiftGen で見る handshake と capabilities

前のモジュールでは、ギフト選定を支援する学習用アプリを構築しました。ウィジェットがあり、バックエンドに suggest_gifts というツールがあり、ギフトのカタログがあります。ここでは MCP サーバー gift-genius の handshake がどう見えるかを想像してみましょう。

GiftGen の handshake 例

クライアントからのリクエスト:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

サーバーからの応答:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

本質的には公式アーキテクチャの例を、私たちのアプリ名に合わせているだけです。

この応答からクライアントが得ること:

  • ツール(tools)があり、一覧は動的に変わる可能性がある(listChanged: true)。
  • リソースがある(ギフトのカタログ。ファイルや DB に保存されているかもしれない)。
  • プロンプトがある(例: 「ユーザー N 向けの短いギフト説明を作成せよ」というテンプレート)。
  • サーバーはログを送れる(インスペクタやデバッグに便利)。

続いてクライアントは tools/list を実行し、たとえば次のようなツールを確認します:

{
  "name": "suggest_gifts",
  "description": "受取人のプロフィールに基づいてギフトのアイデアを提案します。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

そしてユーザーが「25 歳の姉向けに、予算 50 ドル以内でプレゼントを提案して」と書いたとき、モデルは「suggest_gifts というツールがあり、これこれの引数を持つ」と理解しており、tools/call 経由で呼び出せるとわかっています。

8. SDK は handshake を隠蔽する(それでも理解が重要な理由)

次回使う TypeScript 向け MCP SDK では、initializenotifications/initialized の一連は connect メソッドの中に隠蔽されています。おおよそのコードは次のとおりです:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// ツールの登録 – SDK はこれに基づいて capabilities.tools を自動設定する
server.tool(
  "suggest_gifts",
  {
    description: "ギフトのアイデアを提案します。",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... ギフト選定のロジック ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// ここで SDK が行うこと:
// 1) クライアントから initialize を受け取る
// 2) serverInfo と capabilities で応答する
// 3) notifications/initialized を待つ
// 4) その後 tools/* 呼び出しの処理を開始する
await server.connect(transport);

SDK は、あなたが登録した内容に基づいて capabilities を自動生成します。少なくとも 1 つの server.tool(...) があれば、capabilities に tools セクションが追加されます。リソースやプロンプトを登録すれば、resourcesprompts も現れます。

Handshake と capabilities の理解は、手で JSON を書くため(決してそうしないでください)ではなく、次のために必要です:

  • MCP ログを読み、クライアントがツールを「見えない」理由を理解する。
  • プロトコルバージョンの非互換を診断する。
  • 必要に応じてカスタムサーバーや非標準トランスポートを実装する。

9. プロトコルバージョンと機能の進化

Handshake の protocolVersion は飾りではありません。MCP 仕様は明確に述べています。これは互換なプロトコルバージョンを取り決める手段であり、共通バージョンが見つからなければ接続は終了すべきだと。

典型的なシナリオ:

  1. MCP サーバーを本番にデプロイしており、使っている SDK は MCP の "2025-06-18" を実装している。
  2. しばらくして MCP の新バージョンが出て、クライアントを更新したがサーバーはまだ古い。
  3. クライアントが protocolVersion"2026-02-01" を送るが、サーバーはそのバージョンを知らず、invalid protocol version(もしくは同等の)エラーを返す。

実務ではこのフィールドを軽視しがちで、その結果、接続が確立しない理由に気づかないことがあります。

バージョンへの正しい向き合い方:

  • 使用している SDK がどの MCP バージョンに対応しているか(通常はドキュメントやリリースノート)を常に把握する。
  • SDK を更新したら、意識的にプロトコルバージョンも更新する。
  • ログやモニタリングで、protocolVersion 不一致による初期化エラーを明示的に見えるようにする。

capabilities による拡張も進化と結びついています。MCP の新機能は capabilities の新しいキーとして追加されます。古いクライアントはそれを無視し、新しいクライアントは活用できます。このパターンこそが、MCP 公式ドキュメントで後方互換性を保つ方法として説明されているものです。

10. ChatGPT とインスペクタの視点から見た handshake

ChatGPT は MCP 接続時に何をするか

Dev Mode で MCP サーバーを ChatGPT に紐づけると、プラットフォームは裏側で概ね次を行います:

  1. トランスポートを開く(多くは /mcp への HTTP/stream)。
  2. initialize を送り、protocolVersioncapabilitiesclientInfo(「ChatGPT Enterprise、バージョン〜」のようなもの)を含める。
  3. 応答を受け取り、サーバーの capabilities をキャッシュする。
  4. 見えた capabilities に応じて、tools/listresources/listprompts/list を実行する。
  5. 対話中にモデルがツールを呼ぶ必要があると判断したら、このキャッシュを参照して、ツールの有無、引数スキーマ、呼び出し方法を確認する。

サーバーの capabilities に tools が含まれていない場合、ChatGPT はあなたの App をツールとして提案しようとすらしません。resources はあるが listChanged フラグがない場合、ChatGPT はリソース一覧をキャッシュし、変更通知を待たないことがあります。

インスペクタや MCP Jam はデバッグにどう役立つか

MCP Jam / MCP Inspector のようなツールも実質的に同じことをします。接続を確立し、handshake を実行し、サーバーの capabilities を見せ、手動で tools/listtools/call などを呼べるようにしてくれます。

開発者にとっては必携:

  • サーバーが実際に返した protocolVersion が見える。
  • capabilities に toolsresourcesprompts があるか一目でわかる。
  • ChatGPT がツールを見つけられない理由(capabilities が宣言されていない、または handshake が通っていない)を把握できる。

このモジュールの最後の講義では、こうしたツールをより集中的に使いますが、彼らが動いているのはまさに本講義で扱った handshake の上だと、今のうちから理解しておくと良いでしょう。

11. handshake と capabilities でよくあるミス

理屈はシンプルに見えますが、実際には handshake と capabilities の宣言が、特に Dev Mode や MCP Inspector で、初歩的なバグの温床になりがちです。以下は、あなた自身か同僚のログで間違いなく遭遇するであろう典型的なエラーです。

エラー №1: initialize リクエストの形式が不正。
SDK を使わずに MCP サーバーを手実装するときに非常にありがちなのが、JSON-RPC の必須フィールドをどれか落としてしまうことです。例えば jsonrpc: "2.0" を入れ忘れる、method を取り違える("initialize" の代わりに "init" と書く)、capabilities をオブジェクトではなくブールで送ってしまう、など。MCP 仕様は厳密な形式を期待しており、逸脱はパースエラーと接続断につながります。ドキュメントや実践ガイドでも、まず initialize が仕様どおりか確認するよう強く勧めています。

エラー №2: protocolVersion を無視する。
開発者がドキュメントの例をそのままコピペして、SDK の対応を見ずに適当な文字列を入れてしまうことがあります。結果、クライアントとサーバーが別の MCP バージョンを話し、接続が確立しません。「クライアントが全くつながらない」という症状で現れることもあります。protocolVersion は現実の契約だと捉え、フロントエンド/エージェントプラットフォームと MCP サーバー側のチームで一致させてください。

エラー №3: capabilities の入れ忘れ。
ありがちな状況です。サーバーにツールを登録したのに、handshake を手実装する過程で capabilities 応答に "tools": {} を入れ忘れる。Inspector ではツールが見えるのに、ChatGPT は「No tools available」と表示する — これは ChatGPT が capabilities を信頼しており、tools セクションがなければ tools/list を呼ばないからです。Apps SDK のトラブルシューティングガイドでも強調されています。ChatGPT がツールを見ないときは、真っ先に capabilities を確認せよ、と。

エラー №4: capabilities に宣言していないメソッドを使おうとする。
たとえば resources セクションが capabilities にないサーバーに、resources/list を送る、などの実験を学生がしがちです。形式上サーバーは Method not found と返すかもしれませんが、そもそも呼ぶべきではありません。MCP はこの種の誤用を防ぐために capabilities を導入しています。クライアントはまず capabilities に該当セクションがあるか確認し、そのうえでメソッドを呼ぶべきです。

エラー №5: notifications/initialized を待たずにサーバーが「しゃべり出す」。
サーバーが initialize 応答直後に、notifications/initialized を待たずログや通知を送り始めると、一部のクライアントはそれらを無視したり、接続を切ることすらあります。MCP の公式アーキテクチャでは、まず handshake を完了し、初期化通知の後で「実務」フェーズに入るべきだと強調されています。

エラー №6: リスト変更シグナルなしでツールのスキーマを変更する。
ツールの JSON Schema を変更(必須フィールド化、引数名の変更など)したのに、サーバーを再起動しない、あるいはツール一覧が更新された通知を送らないと、クライアントのキャッシュが古いスキーマのままになります。これが奇妙なバリデーションエラーを生みます。仕様では、クライアントのキャッシュ更新を助けるために listChanged フラグと tools/list_changedresources/list_changed 通知の利用を提案しています。

エラー №7: 早すぎる最適化や capabilities 周りの「魔法」。
基本を理解しないまま、クライアント別の動的 capabilities 生成や複雑なバージョニングなどに走るケースがあります。最初はサーバーができること(tools、resources、prompts、logging)を正直に宣言するだけで十分です。capabilities の拡張は、実際の必要が生じたときに行いましょう。これは組織的なアンチパターン寄りの話ですが、実案件で非常に頻繁に見られます。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION