MCPとfunction calling
今回の記事では、MCP(Model Context Protocol)がfunction callingと何が違うのかについて話してみたい。
Claude CodeやCursorにMCPサーバーをつないで使ってはいるものの、それがLLM APIのfunction callingとどこで分かれるのかを説明しにくかった開発者に向けた記事だ。先に答えを書くと、MCPはfunction callingを置き換えない。ホストアプリケーションがMCPサーバーから受け取ったツール一覧をfunction callingのtoolsパラメータに変換して渡し、モデルが選んだ呼び出しを再びMCPサーバーへ渡す。最後まで読めば、その上にMCPが加える4つの違いがプロトコルのどの部分から来ているのか、2026-07-28改訂版の後も残る違いは何か、そしてその構造がどのような攻撃面を開くのかがわかる。
筆者はフロントエンド開発者として日常的にClaudeを活用しているが、MCPサーバーを一つ追加するたびに、これらのツールがどのような仕組みでモデルの視野に入るのかが毎回あやふやだった。
MCP(Model Context Protocol)
MCP(Model Context Protocol)は「エージェントに何ができるようにするか」という問題を解く。
少し具体的に説明しよう。AIエージェントがSlackへメッセージを送るには、Slack APIを呼び出せなければならない。GitHub Issueを作るには、GitHub APIを呼び出せなければならない。Postgresへクエリするには、DB接続を扱えなければならない。こうした外部システムとの統合を、一つの標準プロトコルにまとめたものがMCPだ。(クライアントとサーバーが同じ規格で接続されるという意味だ。)
MCPはAnthropicが2024年11月25日に初めて公開したオープン標準だ。そして2025年12月9日、AnthropicはMCPをLinux Foundation傘下のAgentic AI Foundation(AAIF)へ寄贈した。AAIFはAnthropic・Block・OpenAIが共同で創設した。
MCPはJSON-RPC上に構築されたプロトコルだ。JSON-RPC 2.0は、JSONをワイヤーフォーマットとして使うstatelessで軽量なRPC(Remote Procedure Call)プロトコルである。トランスポート層に依存せず、HTTP・TCP・標準入出力のいずれでも動作する。この記事は2025-11-25改訂版を基準に説明する。この版のMCPは、接続ごとにセッションを張るstatefulなプロトコルだ。その後の改訂版で何が変わったのかは、呼び出しの流れを見てから扱う。
6つのプリミティブ
2025-11-25仕様の概要は、サーバーが提供する機能3つと、クライアントが提供する機能3つを挙げている。この記事ではこの6つをプリミティブ(primitive)と呼ぶ。ここでいうプリミティブは、JavaScriptのプリミティブ型(stringやnumberなど)とは関係がなく、プロトコルが定めた基本的なやり取りの種類を指す。
サーバー側プリミティブ
- Tool(model-controlled):モデルが呼び出すかどうかを自ら判断して実行する操作。この操作は副作用(side effect)を持つことがある
- Resource(application-controlled):URIで識別されるデータ。仕様には内容を読み出す
resources/readだけがあり、書き込むメソッドはない。そのリソースをコンテキストにどう入れるかはホストアプリケーションが決める - Prompt(user-controlled):ユーザーがスラッシュコマンドなどで明示的にトリガーする、再利用可能なテンプレート
クライアント側プリミティブ
- Sampling:サーバーから逆にクライアントのLLMへcompletionを要求できる仕組みで、クライアントとサーバーを双方向の構造にする。ツールの実行中に文章の生成が必要になったサーバーが、自前のAPIキーなしでクライアントの使うモデルを借りるための仕組みだ。2026-07-28改訂版では削除予定のdeprecatedになった
- Roots:クライアントがサーバーへ「ここまでが作業可能な範囲」と伝えるワークスペース境界の情報
- Elicitation:サーバーがツールの実行中に、構造化された形式でユーザーへ追加入力を求められる機能
この区別が重要なのは、誰が呼び出しや提供を決めるのかが異なるからだ。Toolはモデルの判断で実行されるため誤った呼び出しのリスクがあり、Promptはユーザーが明示的に選ぶ。Resourceはアプリが選ぶのが基本だが、仕様はヒューリスティクスやモデルの選択による自動的な取り込みを行う実装も認めている。クライアント側の3つは向きが逆だ。サーバーが要求し、応じるかどうかはクライアントが決める。
2つの転送方式
標準の転送方式は2つで、仕様はそれ以外のカスタム転送も認めている(MAY)。一つはstdioで、MCPサーバーをローカルのサブプロセスとして実行し、標準入出力で通信する方式だ。ファイルシステムやGitなど、ローカルで動くツールに適している。もう一つはStreamable HTTPで、HTTP POSTとGETにSSE(Server-Sent Events)ストリーミングを重ね、双方向に近い通信を実現する方式だ。SSEは、HTTP接続の上でサーバーからクライアントへ一方向にデータを送り出す方式だ。リモートサーバー、OAuth認証、複数クライアント接続、クラウドデプロイなど、ネットワーク越しのシナリオに適している。
LLMがMCPツールを呼び出す流れ
プリミティブと転送方式を確認したので、次は実際にLLMがMCPツールをどのように発見し、呼び出すのかを追ってみよう。
2025-11-25改訂版では、接続が始まると次の順序でハンドシェイクが行われる。2026-10-08時点で、TypeScript SDKの1.32.1とv2系の2.3.1も、既定の設定ではこの順序で動く。
- クライアント → サーバー:
initializeリクエスト(対応するプロトコルバージョンとクライアントcapabilitiesを渡す) - サーバー → クライアント:
initializeレスポンス(サーバーcapabilitiesと、任意のinstructionsフィールド) - クライアント → サーバー:
notifications/initialized通知 - クライアント → サーバー:
tools/listリクエスト → 利用可能なツール一覧を受信 - (以後)LLMがツールを呼び出すと判断 → クライアントが
tools/callを送信 → 結果を受信
initializeレスポンスのinstructionsフィールドは、サーバーがツールの使い方をテキストで書いて送る場所だ。下のデモ出力のinstructions行がこの値である。
では、tool定義そのものはどのようにLLMの視野に入るのか。MCPのtool定義は、次のようなJSON Schemaの形をしている。
{
"name": "get_weather",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"]
}
}ホストはtools/listで受け取ったこの一覧を、Anthropic Messages APIのtoolsパラメータまたはOpenAI function callingのtoolsパラメータへ変換し、LLM APIの呼び出し時に一緒に渡す。Anthropicの場合、toolパラメータが渡されるとspecial system promptが自動的に追加され、モデルがtoolの呼び出し方を理解できるようになる。その長さはモデルによって異なる。2026-10-08に見たドキュメントの表では、tool_choice: autoの場合、現行モデルで286〜675トークンだった。
LLMがツールを呼び出す必要があると判断すると、レスポンス内にtool_useブロック({"type": "tool_use", "name": ..., "input": ...})が挿入され、レスポンスのstop_reasonはtool_useで終わる。ホストはこれを受け取り、実際のMCPサーバーへtools/callを送信する。結果を受け取ると、次のuserメッセージのtool_resultブロックに入れて、再びLLMへ送る。stop_reasonがtool_use以外の値(end_turn、max_tokensなど)に変わるまで、このループが繰り返される。 私たちが一般に「エージェントが働く」と呼ぶ動作は、実際にはこの呼び出し・結果・呼び出しのループが連続することに近い。
図にすると、モデルとMCPサーバーの間には線がない。モデルはtoolsで受け取った定義だけを見て、MCPサーバーはモデルではなくホストからリクエストを受ける。2つのプロトコルをつなぐのは真ん中のホストだ。

この変換が実際にどれほど短いのかを動かして確かめた。2026-10-08にNode v24.16.0、@modelcontextprotocol/sdk 1.32.1、zod 4.6.5で実行した。転送はstdioではなく、同じプロセス内でサーバーとクライアントをつなぐInMemoryTransportで、LLMは呼び出していない。tool_useブロックはAnthropicのドキュメントの形どおりに手で作った。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { z } from "zod";
// MCP 서버: 도구 하나와 instructions 를 둔다
const server = new McpServer(
{ name: "weather", version: "1.0.0" },
{ instructions: "Use get_weather for current conditions only." }
);
server.registerTool(
"get_weather",
{ description: "Get current weather information for a location", inputSchema: { location: z.string() } },
async ({ location }) => ({ content: [{ type: "text", text: `${location}: 15C, partly cloudy` }] })
);
// 호스트: 같은 프로세스 안에서 서버와 잇고, 클라이언트가 보내는 메서드 이름을 찍는다
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
const send = clientT.send.bind(clientT);
clientT.send = (m) => { console.log("C->S", m.method, m.params?.protocolVersion ?? ""); return send(m); };
await server.connect(serverT);
const client = new Client({ name: "demo-host", version: "1.0.0" });
await client.connect(clientT);
console.log("instructions:", client.getInstructions());
// 1. tools/list 결과를 LLM API 의 tools 파라미터 모양으로 바꾼다
const { tools } = await client.listTools();
const anthropicTools = tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema }));
const openaiTools = tools.map((t) => ({ type: "function", name: t.name, description: t.description, parameters: t.inputSchema }));
console.log("anthropic:", JSON.stringify(anthropicTools[0]));
console.log("openai:", JSON.stringify(openaiTools[0]));
// 2. 모델이 이런 tool_use 블록을 돌려줬다고 가정한다. input 이 그대로 tools/call 의 arguments 가 된다
const toolUse = { type: "tool_use", id: "toolu_demo", name: "get_weather", input: { location: "Seoul" } };
const result = await client.callTool({ name: toolUse.name, arguments: toolUse.input });
console.log("tool_result:", JSON.stringify({ type: "tool_result", tool_use_id: toolUse.id, content: result.content }));
await client.close();node post-demo.mjsの出力は次のとおりだ。
C->S initialize 2025-11-25
C->S notifications/initialized
instructions: Use get_weather for current conditions only.
C->S tools/list
anthropic: {"name":"get_weather","description":"Get current weather information for a location","input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"$schema":"http://json-schema.org/draft-07/schema#"}}
openai: {"type":"function","name":"get_weather","description":"Get current weather information for a location","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"$schema":"http://json-schema.org/draft-07/schema#"}}
C->S tools/call
tool_result: {"type":"tool_result","tool_use_id":"toolu_demo","content":[{"type":"text","text":"Seoul: 15C, partly cloudy"}]}ツール定義の変換はフィールド名を変えるだけだ。MCPのinputSchemaがAnthropicではinput_schema、OpenAIではparametersになる。SDKがスキーマに$schemaを付け足すことも出力からわかる。逆方向も短い。Anthropicのtool_use.inputはオブジェクトなので、そのままtools/callのargumentsになる。結果の側も、textブロックは形が同じなのでそのまま渡せるが、画像やエラーの結果は形が異なるため、Anthropic SDKのMCP helperが別途変換する。上のOpenAI側の形はResponses APIの形式だ。OpenAIが返す呼び出しのargumentsはJSON文字列なので、渡す前にJSON.parseを一度通す必要がある。上のコードはAnthropic形式のtool_useブロックしか作っていないので、このparseの段階は出力に現れない。
MCPが加える4つのもの
では、MCPはfunction callingに何を加えるのか。2025-11-25改訂版を基準にすると4つある。
- 動的な発見:ビルド時にはツール一覧を知らず、実行時に
tools/listで取得する。サーバーはnotifications/tools/list_changedで、接続中に一覧が変わったことを知らせられる - Stateful session:
initializeで接続を確立し、その中でリクエストがやり取りされる。終了用のJSON-RPCメッセージはなく、転送を閉じることで終える - Tool以外のプリミティブ:Resource・Prompt・Sampling・Roots・Elicitationをcapability negotiationで公開する。capability negotiationとは、
initializeで双方が対応する機能を互いに知らせる段階だ - 双方向性:サーバーがSamplingを使って、クライアントのLLMへ逆にcompletionを要求できる(2026-07-28改訂版でdeprecated)
2026-07-28改訂版以降
2026-10-08時点で、公式サイトでlatestとして開くのは2026-07-28改訂版で、ここでこのリストの半分が変わった。まず、initializeとnotifications/initializedからなるハンドシェイクとプロトコルレベルのセッションがなくなった。代わりに、すべてのリクエストが_metaにプロトコルバージョンとクライアントcapabilitiesを載せる。このフィールドは、メッセージ本来の引数とは別にメタデータを付けるためにMCPが予約している場所だ。サーバーはserver/discoverを必ず実装しなければならない(MUST)。サーバーが対応するプロトコルバージョン、capabilities、サーバー情報を返すRPCで、クライアントはほかのリクエストより先にこれを呼び出し、対応バージョンとcapabilitiesを事前に確認できる。
サーバーが先に送っていたリクエストは、Multi Round-Trip Requests(MRTR)というパターンに置き換えられた。サーバーは別途リクエストを送る代わりに、追加の入力が必要だという中間結果(input_required)を返し、クライアントがその入力を埋めて元のリクエストを送り直す方式だ。
SamplingとRootsはLoggingとともにdeprecatedになった。仕様に残っていて動作もするが新しい実装は採用すべきでないという意味で、仕様はSamplingの代わりにLLMプロバイダーのAPIへ直接つなぐよう勧めている。
ただし、SDKの既定の動作はまだ古い方式だ。TypeScript SDK 1.32.1は最新バージョン定数が2025-11-25なので2026-07-28改訂版を知らず、2.3.1はこの改訂版に対応しているが、バージョン交渉の既定値がlegacyだ。上の出力の1行目initialize 2025-11-25がその結果である。
すると、4つのうち残るのは動的な発見とTool以外のプリミティブ(Resource、Prompt、Elicitation)だ。動的な発見も形が少し変わり、一覧の変更通知はsubscriptions/listenストリームにopt-inしたクライアントだけが受け取る。結局、MCPがfunction callingと分かれるところは、セッションや双方向性よりも、ツール一覧とコンテキストを実行時にやり取りする契約にある。
APIがMCPクライアントになるとき
この契約のうちどこまでがモデルに届くのかは、AnthropicのMCP connector(beta)が示している。Messages APIがリモートのMCPサーバーに直接つなぐ機能だが、ドキュメントのLimitationsは、MCP仕様の機能のうち"only tool calls are currently supported"と書き、"Local STDIO servers cannot be connected directly"と書いている。同じドキュメントは、ローカルサーバーやMCPのprompt、resourceが必要なら、MCP SDKで接続を自分で管理しながらAnthropic SDKの変換helperを使うよう案内している。
つまり、function callingのレイヤーでMCPを消費するとToolだけが残る。ResourceとPromptは、それを画面やコンテキストへ運ぶホストがあって初めて意味を持つ。OpenAIもfunction callingガイドで、MCPサーバーの機能をbuilt-in toolとして使う方法を紹介している。OpenAIのMCP serversガイドはツール一覧の取得と呼び出しの方法だけを説明し、ResourceやPromptに対応しているかどうかは書いていない。
動的な発見が開く攻撃面
ツールのdescriptionとツール呼び出しの結果は、ホストを経てモデルのcontextに入る。だからサーバーがそこに何を書いても、モデルはそれを読む。代表的な2つの攻撃は、どちらもここから生まれる。
-
Tool Poisoning Attack(TPA):Invariant Labsが2025年4月に命名し、PoCを公開した攻撃だ。MCPサーバーのツール説明(description)に悪意ある指示を隠すと、モデルはユーザーには見えないそのテキストを読み、ユーザーの知らないうちに従うことがある。
-
Rug Pull:Invariant Labsが同じ記事で説明した攻撃で、ユーザーが承認した後にサーバーがツール定義を変える。Simon Willisonが引用したElena Crossの例のように、1日目に安全そうに見えるツールを承認したのに、7日目にはそのツールがAPIキーを攻撃者へ送るように変わっている、という具合で、ツール定義をインストール時点ではなく実行時にサーバーから受け取る構造から生まれる。tool仕様は、どのツールがモデルに公開されているかを示すUIを推奨する(SHOULD)だけで、変わった定義の再承認までは求めていないので、再承認はホストの役目になる。
まとめ
まとめると、MCPはfunction callingを置き換えるものではなく、その上に載った標準だ。モデルがツールを呼び出す方法は依然としてtoolsパラメータとtool_useループであり、2つのプロトコルの間はホストが翻訳する。2025-11-25改訂版でMCPが加えたのは、動的な発見、stateful session、Tool以外のプリミティブ、そしてサーバーからクライアントへ向かう呼び出しだった。2026-07-28改訂版でセッションがなくなりSamplingがdeprecatedになると、残るのはツール一覧とコンテキストを実行時にやり取りする契約だ。その契約があるからこそ、ツール定義を汚染したり、こっそり書き換えたりする攻撃も同じところから生まれる。MCPサーバーをもう一つつなぐときは、そのサーバーが何をできるのかとあわせて、定義が変わったときにホストが知らせてくれるかどうかも確かめてみてほしい。
MCPがエージェントに何をできるようにするかの問題なら、何を知らせるかはCLAUDE.mdやAGENTS.mdのようなコンテキストファイルの問題だ。それらのファイルがエージェントにどのように読まれ、どこまで守られるのかはコンテキストファイルで扱う。