MCPの「server/discover」はサーバーの何を返すRPCか
MCPの2026-07-28版仕様で追加されたserver/discoverは、対応プロトコルバージョン・機能・サーバー識別を1回のリクエストで返す必須RPCです。initializeハンドシェイクとの違いと使いどころをまとめます。
server/discoverはサーバーの対応バージョン・機能・身元を1回で返すRPC
server/discoverは、MCPの2026-07-28版仕様で新設されたRPCです。クライアントは他のリクエストより先にこれを呼ぶと、サーバーが対応するプロトコルバージョン・機能(capabilities)・識別情報をまとめて受け取れます。サーバー側の実装はMUST(必須)です。
リクエストは標準の_meta以外にボディパラメータを持ちません。
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}応答の中身は次の4つです。supportedVersionsは、クライアントが後続のリクエストで選ぶバージョンの候補です。capabilitiesはtools・resources・promptsなどの対応状況、_metaのserverInfoはサーバーソフトの名前とバージョンで、仕様はこれを含めることをSHOULDとしています。instructionsは、LLMに向けてそのサーバーの使い方を伝える自然文で、省略できます。
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "resources": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }
},
"instructions": "This server provides weather and resource utilities.",
"ttlMs": 3600000,
"cacheScope": "public"
}
}呼ばなくても動く — それでも呼ぶ場面は2つ
クライアントがserver/discoverを呼ぶかどうかは任意です。いきなりtools/listなどを送り、サーバーが対応していなければ返るUnsupportedProtocolVersionErrorを見て判断する運用も許されています。
呼ぶ価値がある2つの場面
サーバー情報の一括表示
tools/list・prompts/list・resources/listを個別に呼んで機能を推し量る代わりに、1回の応答でサーバーの身元・対応機能・対応バージョンを画面に出せます。stdioでの新旧判定
stdioにはHTTPのステータスコードのような手がかりがありません。新旧両方のサーバーに対応するクライアントは、先に
server/discoverを送って相手の世代を見分けます(仕様はSHOULD)。
stdioのプローブは3通りの結果で分岐する
stdioの仕様は、プローブの結果を3通りに分けています。判定の軸は「応答の中身」で、特定のエラーコードではありません。
server/discoverを先に送った後の判断
- 1
DiscoverResultが返る
相手はモダンなサーバーです。
supportedVersionsから双方が対応するバージョンを選んで続けます。 - 2
UnsupportedProtocolVersionErrorなどモダン側の既知エラーが返る
相手はモダンですが、要求したバージョンには対応していません。エラーの
supportedにある版を選び直します。initializeへのフォールバックはしません。 - 3
それ以外のエラーが返る、または時間内に応答がない
相手はレガシーなサーバーです。
initializeハンドシェイクに切り替えます。
フォールバックを特定のエラーコードに結びつけてはいけない(MUST NOT)と仕様は明記しています。レガシーなサーバーは、未知のリクエストに実装ごとに違うエラー(-32601や-32602がよくある例)を返すか、そもそも何も返さないためです。
モダン専用のクライアントでも、プローブは推奨(RECOMMENDED)です。レガシーなサーバーの中には、initializeより前に来たリクエストを検証せず、tools/callのような世代をまたいで解釈できるメソッドをレガシーの意味論で処理してしまうものがあります。先にプローブすれば、そうした暗黙の誤処理ではなく決まった形の失敗になります。
判定した世代は、stdioならサーバープロセスの存続中、HTTPならオリジンごとにキャッシュしてよく(SHOULD)、設定が同じなら再起動をまたいで保存してもかまいません。前提が後で崩れたら再プローブします。
initializeとの違いは状態を持たないこと
2026-07-28版では、接続の最初に一度だけ行うinitialize/notifications/initializedのハンドシェイクが仕様から消えました。代わりにプロトコルバージョンとクライアントのケーパビリティを、すべてのリクエストの_metaに毎回載せます。
initializeは接続の状態を確立する手続きでしたが、server/discoverは状態を伴わない問い合わせです。状態を作らないので、呼ぶ前後で後続リクエストの書き方は同じです。毎回の_metaが前提を運びます。セッションを前提にしない設計は、同じ改訂でセッションIDが削除された流れとも合っています(詳細はMCPのセッションID(Mcp-Session-Id)はなぜ廃止されたのか)。
_metaに載るバージョン宣言とsupportedVersionsの役割は、次のとおり対になっています。
- バージョンの選択。クライアントは
io.modelcontextprotocol/protocolVersionでそのリクエストの版を宣言し、サーバーはsupportedVersionsで受け付ける版の一覧を返します。 - 機能の発見。クライアントは
clientCapabilitiesを毎回宣言し、サーバーはcapabilitiesを返します。{"tools": {"listChanged": true}}なら、tools機能とツール一覧の変更通知に対応していると分かります。拡張機能の対応表明はcapabilities.extensionsで行います。仕組みはMCPの拡張機能ネゴシエーションはどう合意形成されるかにあります。 - 身元の交換。
clientInfoとserverInfoはデバッグや互換性確認用の情報で、セキュリティ判断には使えません(後述)。
discoverを呼ばずに送ったときのエラー
server/discoverを省いてtools/callなどを送り、サーバーが要求バージョンに対応していないと、次のエラーが返ります。HTTPでは、この応答のステータスコードは400 Bad Requestでなければなりません。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}data.supportedに対応バージョンの一覧が入るので、クライアントは自分も対応する版を選んで同じリクエストを再送できます。
新旧クライアント・サーバーの組み合わせ
versioning仕様の相互運用マトリクスのうち、server/discoverが効く行を抜き出します。
| クライアント | サーバー | 結果 |
|---|---|---|
| モダン | サーバーモダン | 結果動作する。server/discoverは任意で、版の不一致はUnsupportedProtocolVersionErrorで表に出る |
| モダン | サーバーレガシー | 結果失敗する。stdioで先にserver/discoverを送ると、決まった形で失敗させて意味のあるエラーを出せる |
| レガシー | サーバーモダン | 結果失敗する。レガシー側には、モダンへ切り替える手段がない |
新旧両対応のクライアントは、上の分岐のとおりプローブで世代を見分けるので、どちらのサーバーとも動作します。プローブの価値は、モダンのクライアントとレガシーのサーバーの行に集中しています。
応答はキャッシュの対象になる
server/discoverは、tools/list・prompts/list・resources/list・resources/templates/list・resources/readと並ぶ、キャッシュヒント付きの操作です。サーバーはresultType: "complete"の結果にttlMsとcacheScopeを含めなければなりません(MUST)。
ttlMsは、結果を新鮮とみなしてよい時間(ミリ秒)で、HTTPのCache-Control: max-ageに近い意味です。
0なら、すぐに古くなるものとして扱います。- 正の値なら、受信からその時間はキャッシュを使えます。
- 欠けているときは
0とみなします。仕様はこれを旧サーバーに限った状況と説明しています。 - 負の値は
0として扱います。サーバーは0以上の値を返さなければなりません(MUST)。
保証ではなくヒントなので、サーバーはTTLの途中でも中身を変えられます。クライアントはTTLをポーリング間隔として自動再取得に使わず(SHOULD NOT)、データが必要になった時点で鮮度を見て、古ければ取り直します。更新通知(listChanged)が届けば、まだTTL内でもキャッシュは即座に古い扱いになります。
cacheScopeは、誰がキャッシュを持てるかを決めます。
cacheScopeの2値
public
特定のユーザーに依存しないデータです。クライアントだけでなく共有ゲートウェイやキャッシュプロキシも、どのユーザーにもこの応答を返してかまいません。ツール一覧がユーザーによらず同じ場合が例です。
private
利用者固有のデータを含みます。共有できる単位は同じ認可コンテキストの中だけで、アクセストークンが別なら別のキャッシュを持たなければなりません(MUST NOT共有)。ユーザーごとに絞り込まれた一覧などが例です。
privateの応答をpublicとして扱うと、別の利用者向けの内容が他のトークンの持ち主に返りかねません。逆にpublicで済む応答をprivateにすると、共有キャッシュが効かなくなります。
TTLと更新通知、ページ送りの組み合わせ
ttlMsとlistChangedは、片方だけでも併用でも使えます。サーバーがlistChanged: trueを宣言せずにttlMsだけを返してもよく(MAY)、その場合クライアントは鮮度の判断をTTLだけに頼ります。両方を出すなら、クライアントは通知と通知の間の無駄な再取得をTTLで避けつつ、通知を即時の無効化の合図に使えます。
ページ送りされる一覧では、各ページが独立してキャッシュされます。ページごとに自分のttlMsを持ち、鮮度の時計も、そのページを受信した時点から数えます。変化の少ない一覧の前半ページは長いTTL、最後のページは短いTTLにするといった使い分けも許されています(MAY)。期限が切れたページは、クライアントがカーソルを使ってそのページだけ取り直します(SHOULD)。ページをまたいだ一貫性は保証されないので、取得の合間にデータが変われば、ページごとに世代の違う内容が混ざりえます。
serverInfoは自己申告であり、検証されない
_meta['io.modelcontextprotocol/serverInfo']は、サーバー自身が名乗る名前とバージョンです。仕様は、プロトコルが検証した値ではなく、表示・ログ・デバッグのためのものだと注記しています。クライアントは、この値で挙動を変えたり(SHOULD NOT)、セキュリティ判断の根拠にしたり(SHOULD NOT)してはいけません。
新旧両対応サーバーは最初のリクエストの形で世代を選ぶ
旧仕様と2026-07-28版の両方を提供するサーバー(仕様は「dual-era」と呼びます)は、クライアントの開き方で挙動を切り替えます。
_metaを伴うモダンなリクエストは、この改訂に従ってステートレスに処理します。initializeが来たら、レガシーの意味論を選びます。その範囲は、stdioならそのプロセス、HTTPならそのセッションです。
同じエンドポイントやプロセスで、両方の世代を同時に提供してもかまいません(MAY)。
モダンのみのサーバーは、initializeに返すエラーに対応バージョンを書くべき(SHOULD)とされています。レガシーなクライアントにはモダンへ切り替える手段がなく、このメッセージが利用者に見せられる唯一の手がかりになりうるからです。
まとめ
server/discoverは呼ばなくても動くので、実装上の判断は「stdioで新旧を見分ける必要があるか」と「応答をキャッシュに乗せるか」の2点に絞られます。stdioでレガシーが混ざりうるなら先にプローブし、cacheScopeは認可コンテキストをまたいで共有してよい応答かどうかで決めます。