MCPを主に仕様面において見てみたメモ
はじめに
ここ半年くらいMCP(Model Context Protocol)がWebエンジニア界隈を賑わせており、
大まかには理解していたものの、どのような仕様かはほとんどわかっていませんでした。
ちょうど仕事が変わるタイミングで少し余裕ができたので、MCPの仕様を見てみることにしました。
基本的に公式ドキュメントをベースに、自分の理解を書いていくだけの内容のため、特に新しい情報等はここには存在しません。
MCPとは
Anthropicの公式ドキュメントによると、
Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools
ということです。日本語に訳すと、
Model Context Protocol (MCP) は、LLMアプリケーションと外部データソースおよびツールの間で滑らかな統合を可能にするオープンプロトコルです。
とのことです。
類似の例としてOpen AIがChat GPTから外部リソースを操作できるようにするFunction Callingというものがあります。
これだけだとふんわりしているので、具体例を見てみます。
登場人物
MCPというプロトコルにおいては、以下の3つの登場人物が存在します。
- Host: LLMアプリケーション
- Clients: ホスト内のコネクタ
- Servers: ContextとCapability(それぞれ後述)を提供する、(基本的にはホスト外の)サービス
例えばClaudeDesktop上でMCPを利用するケースを考えてみます。
このときの登場人物は以下の3つです。
- Host: Claude Desktop
- Clients: MCPサーバと接続する1プロセスor1コネクションが完了するまでの処理
- Severs: 外部のMCPサーバ(GithubやSlack等)
通信プロトコル
ClientとServerの通信はJSON-RPCで行います。
それらの通信内容のスキーマは以下で定義されています(公式ドキュメント内では具体例はあるが、定義は殆ど書いてない)。
また、伝送路としてstdio(標準入出力)とHTTP(SSE + POST)がプロトコル内では定義されています。
トランスポートレイヤについてのドキュメント
主要機能
MCPの中では5つの主要な機能が定義されています。これらはそれぞれオプショナルです。
Resources
- Server側の機能
- 外部データ(ファイル、データベースの情報、APIのレスポンスなど)をLLMが参照可能な形で提供します。
- 「GitHubリポジトリ内のREADME.mdをモデルに読み込ませて回答に活用する」「PostgreSQLデータベースの内容を参照する」など。
Prompts
- Server側の機能
- LLMに対して渡すプロンプト自体をMCPサーバに生成させます。引数が定義可能です。
- 「この変更内容に対するgitのコミットメッセージを生成してください: {ここに変更内容を入れる}」のようなプロンプトを、スラッシュコマンドのように事前定義されたシンプルな操作で呼び出す、など。
Tools
- Server側の機能
- LLMがテキスト生成以外のアクション(計算、外部API呼び出し、データ処理など)を実行できるようにします。
- 「シェル上でコマンドを実行する」「カレンダーイベントを作成する」など。
Sampling
- Client側の機能
- 外部サーバーがAIモデルによるテキスト生成をリクエストできる機能です。Client側がモデル呼び出しを担当し、結果を返します。
- Claude Desktopでは未サポート。
Roots
- Client側の機能
- MCPにおいて、サーバ側に対して操作可能な範囲を通知するための機能です。どの情報が利用可能で、どのデータが参照可能かを整理します。
- 特定のディレクトリを起点として、その配下にある全てのファイルをコンテキストとして利用可能にしたり、特定のAPIエンドポイントのみ呼び出しを許可するなど。
- URIであれば何でも良いということなので、おそらくDBの接続文字列なども渡してよさそう(最終的にはServer側がそれをどう解釈するか次第だが)
その他、experimentalとしてServer/Client双方ともプロトコルで定義されていない機能を公開することができます。
Capability negotiation
上述したように、MCPにおいてClient側、Server側それぞれ具体的な機能はすべてオプショナルかつ機能の内容も実装によって大きく異なります。
そのため、Serverの初期化時にClient/Serverそれぞれが「どの機能が呼び出せるか」を相互に提示し合う必要があります。
これをCapability negotiationと呼びます。
ライフサイクルの説明より初期化処理のリクエストとレスポンスの例を転載します。
Client->Serverのリクエスト:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
}
}
}
Client<-Serverのレスポンス:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "ExampleServer",
"version": "1.0.0"
},
"instructions": "Optional instructions for the client"
}
}
この中で capabilities
を見ると、このClientがroots
とsampling
に対応していることがわかります。同様にServerは、logging
, prompts
, resources
, tools
に対応していることがわかります。
また、前述した「プロトコルで定義されていない機能」を実装した場合も、このcapabilitiesの中にexperimentalとして提示します。
(主要機能として紹介していませんでしたが、Server側のcapabilityとしてloggingとcompletionというものが存在しますが、詳細については今回は触れません。)
仕様から読み取れないこと
HostとClientの間のプロトコルはどうなっているか
これがない場合、あるClientの実装を他の実装と入れ替えると動かない、という状態になるためHostとClientを概念上分ける理由はないのでは?(もちろん、HostとClientは全く別のものとして実装するのが自然な流れではあるが、プロトコルが定義されないのならHostをあえてMCPの登場人物にする必要はなさそう)
LLMに問い合わせをするのはClientかHostか
例えば dev.to上のとある記事 は
Sampling is the ability for an MCP server to ask the host to run an LLM completion
としています。
また、ドキュメント内の以下の図を見てもSamplingに対してHostがLLMへのリクエストを仲介しているように見えます。
(MCPのSpecificationより引用)
一方でUserGuideのsamplingについての箇所を見ると、
Client samples from an LLM
とだけなっており、Clientが直接LLMにリクエストを投げるように見えます。
また、ドキュメントの実装例の中からsamplingを実装しているClientを2つ見てみましたが、どちらもHostに仲介を任せているようには見えませんでした。
- https://github.com/evalstate/fast-agent/blob/8cae62f8a69839110f6f9bea2948f36663a75497/src/mcp_agent/mcp/sampling.py
- https://github.com/ggozad/oterm/blob/b4933af710ddbaa0256c289835f973bf4c1c607e/src/oterm/tools/mcp/sampling.py
つまり、見た範囲の中では本家が書いたMCPのドキュメント内で矛盾があるように見えるが、実装を見る限りおそらくClientがLLMに直接問い合わせをするように見える、という結論です。
その他: LSPとの比較
MCPとLSP(Language Server Protocol)と比較してみます。
LSPは、エディタとプログラミング言語の組み合わせ問題(いわゆるM×N問題)を解決した成功例です。
MCPとの関係としては、公式ドキュメントにもあるように
MCP takes some inspiration from the Language Server Protocol,
と記載されています。
この点から思想的に近い部分があると考えられるため、今回はLSPと比較してみます。
1. 通信メッセージ構造の比較
LSPもMCPもJSON-RPC 2.0を利用して通信します。
- LSP (補完候補取得リクエスト):
{
"method": "textDocument/completion",
"params": { "textDocument": "...", "position": "..." }
}
- MCP (ツール実行リクエスト):
{
"method": "tools/call",
"params": { "name": "get_weather", "arguments": {...} }
}
また、それぞれstdioを用いた双方向通信を行っています。
2. 初期化時のやり取り(Capability negotiation)
インスパイアされていると言うだけあり、MCPのintializeはLSPのものとかなり近いです。
LSPの例:
{
"method": "initialize",
"params": {
"capabilities": {
"textDocument": { .... }
}
}
}
MCPの例:
{
"method": "initialize",
"params": {
"capabilities": {
"tools": {},
"resources": {"subscribe": true}
}
}
}
まとめ
- MCPはLLMと外部ツール(MCP Server)をつなぐためのプロトコル。
- 機能は大まかなカテゴリに分けて定義されており、通信のスキーマは存在するが、内部で何をやるかは完全に実装依存。
- LSPと同じく、どの機能を実装するかは実装依存となっており、初期化時にCapability negotiationで双方の機能を提示し合う。
ここまでの感想
- LLMを使ったツールは外部リソースへのアクセスのソリューションとしてはFunction Callingなど既存のものもあるが、プロトコルとして定義するというのは今までなかった気がする。
- 現状、かなり薄いプロトコルとして定義されており、とりあえずMCP Serverを実装するだけなら標準ライブラリだけで十分可能そう。
- 仕様が薄いかつふわっとしてる部分もあるので、ざっくり仕様だけ読んでもあまり学びはなく、自分で動かしたり実装してみると良さそう。
- 今回触れていないセキュリティ面の話と、現状の主要なサービスでのMCP対応状況みたいなのも書きたい。
Discussion