Model Context Protocol(MCP)は、LLMクライアントと外部ツール・データソースの接続を標準化するオープンプロトコルだ。本記事では、2026年4月時点で有効な仕様リビジョン 2025-03-26 をベースに、公式 TypeScript SDK を使った最小実装、stdio と Streamable HTTP の使い分け、Claude Code / Claude Desktop / Cursor への接続方法、そして2026年Q2に予定されている次期仕様(SEP)の見通しまでを順に追う。
MCPは何を標準化するプロトコルか
MCPが扱う対象は、LLMクライアント(Claude Desktop、Claude Code、Cursor など)と、外部のコンテキスト提供プロセス(ファイルシステム、DB、社内APIなど)の間の通信である。プロトコルの実体は JSON-RPC 2.0 であり、その上に「どういうメッセージで何を問い合わせ、何を返すか」を規定している。
構成要素は3層に分かれる。Client はLLM側に組み込まれたMCPクライアント実装で、Server の一覧取得(tools/list)やツール実行(tools/call)などを発行する。Transport はそのJSON-RPCメッセージを運ぶ経路で、stdio かHTTPベースの2系統。Server は Client からの要求に応答するプロセスで、以下3種類のプリミティブを公開できる。
- Tools:LLMが副作用付きで呼び出せる関数。スキーマ付きの入力を取り、任意のレスポンスを返す。関数呼び出しの抽象化。
- Resources:URIでアドレッシングされる読み取り専用データ。ファイル、DB行、API応答など「コンテキストとして読ませたいもの」を提供する。
- Prompts:ユーザーが明示的に選択できる、引数付きのプロンプトテンプレート。スラッシュコマンドのバックエンドと考えると近い。
重要なのは、どのプリミティブを公開するかは Server 側の裁量であり、Client は initialize 時に capabilities をネゴシエーションしてから使える機能を決める点だ。ツールだけの Server、リソースだけの Server、両方の Server が混在してよい。
公式トランスポート3種とその使い分け
2026年4月時点で仕様に存在するトランスポートは3つある。実装判断の軸は「Server がローカルプロセスか、ネットワーク越しの独立サービスか」である。
| Transport | ステータス | 用途 |
|---|---|---|
| stdio | 現行 | ローカルで1プロセス1クライアントの接続。最小構成。 |
| HTTP+SSE(legacy) | 非推奨/後方互換のみ | 旧仕様で書かれた Server の互換維持。新規採用は非推奨。 |
| Streamable HTTP | 現行・推奨 | リモート Server。複数クライアントからの多重接続を単一プロセスで処理。 |
stdio は、Client が Server を子プロセスとして起動し、標準入出力でJSON-RPCメッセージを往復させる方式だ。1プロセス1接続で、起動コストが低く、ローカルのファイルやCLIツールを呼ぶ用途に最適化されている。Claude Desktop や Cursor が「ローカルのMCP Server」と言うときは基本的にこれを指す。
HTTP+SSE(旧Transport)は、POSTで要求、SSEで応答ストリームを受ける構造だったが、2025-03-26 リビジョンで Streamable HTTP に置き換えられた。後方互換性のためSDKには残っているが、新規実装で選択する理由はない。
Streamable HTTP は、単一のエンドポイント(慣例的に /mcp)に対して POST と GET の両方を受ける。POST はクライアントから Server へのJSON-RPCメッセージ(単発応答またはSSEストリーム)、GET はサーバー起点イベント用の長命SSEストリームだ。セッションは Mcp-Session-Id ヘッダで維持され、1プロセスの Server が多数のクライアント接続を捌く設計になっている。リモート Server をホスティングする場合は事実上これ一択である。
最小MCP Server を TypeScript SDK で実装する
公式の TypeScript SDK は @modelcontextprotocol/sdk として配布されている。入力スキーマ検証には zod を組み合わせるのが慣例だ。
npm i @modelcontextprotocol/sdk zod
最小構成として、現在時刻を返す get_time ツールと、指定パスのJSONを読み込む read_json ツールの2つを持つ Server を書く。下のコードは src/server.ts に置き、tsc で dist/server.js にビルドすることを前提にしている。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
const server = new McpServer({
name: "local-utils",
version: "0.1.0",
});
server.tool(
"get_time",
"現在時刻をISO8601で返す",
{},
async () => ({
content: [{ type: "text", text: new Date().toISOString() }],
}),
);
const BASE_DIR = resolve(process.env.MCP_BASE_DIR ?? process.cwd());
server.tool(
"read_json",
"BASE_DIR 配下のJSONファイルを読み込む",
{ path: z.string().min(1) },
async ({ path }) => {
const abs = resolve(BASE_DIR, path);
if (!abs.startsWith(BASE_DIR + "/") && abs !== BASE_DIR) {
throw new Error("path traversal denied");
}
const text = await readFile(abs, "utf8");
JSON.parse(text); // 妥当性確認
return { content: [{ type: "text", text }] };
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
押さえるポイントは3つ。第1に、server.tool(name, description, schema, handler) の第3引数は zod スキーマのプロパティ辞書で、これが tools/list のレスポンスに JSON Schema として展開される。LLMはここだけを見て呼び出し方を決めるため、description は利用者ではなくLLMに向けて書くのが正解だ。第2に、read_json で BASE_DIR 外へのパストラバーサルを拒否している。stdio で動いていても、LLMの生成した引数はユーザー入力と同程度には信用できない。第3に、プロセスはstdioでブロッキング的に動き続けるため、await server.connect() 後に明示的な stdin 待ちループは書かない。
Claude Code / Claude Desktop / Cursor に接続する
MCP Server は単独では動作しない。ホストアプリに登録してはじめて Client から見える状態になる。
Claude Code
ターミナルから claude mcp add コマンドで登録する。
claude mcp add local-utils -- node /absolute/path/to/dist/server.js
-- 以降が Server の起動コマンドとして保存される。登録状況は claude mcp list、削除は claude mcp remove local-utils で行う。Claude Code は起動時に stdio で Server プロセスを spawn し、セッション終了時に kill する。
Claude Desktop
Claude Desktop は claude_desktop_config.json の mcpServers キーで設定する。macOSなら ~/Library/Application Support/Claude/claude_desktop_config.json、Windowsなら %APPDATA%\Claude\claude_desktop_config.json が参照される。
{
"mcpServers": {
"local-utils": {
"command": "node",
"args": ["/absolute/path/to/dist/server.js"],
"env": {
"MCP_BASE_DIR": "/Users/me/projects/data"
}
}
}
}
設定を保存したらアプリを再起動する。ツールが認識されると、入力欄のツールアイコンから一覧に get_time read_json が現れる。
Cursor
Cursor はプロジェクト固有の .cursor/mcp.json、またはグローバル設定UI(Settings → MCP)のいずれかで登録できる。フォーマットは Claude Desktop とほぼ同じで、stdio と HTTP の両方を指定可能だ。
{
"mcpServers": {
"local-utils": {
"command": "node",
"args": ["/absolute/path/to/dist/server.js"]
},
"company-api": {
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer ${env:COMPANY_TOKEN}" }
}
}
}
url 指定の場合、Cursor は Streamable HTTP クライアントとして振る舞う。環境変数は ${env:NAME} 構文で埋め込める。
Streamable HTTP への切り替え
リモート配信、つまり複数ユーザーの Client から単一プロセスの Server に接続させたい場合、トランスポートを StreamableHTTPServerTransport に差し替える。Server インスタンスそのものは stdio 版と使い回せる。
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const server = new McpServer({ name: "remote-utils", version: "0.1.0" });
// server.tool(...) は stdio 版と同じ
const transports = new Map<string, StreamableHTTPServerTransport>();
const app = express();
app.use(express.json());
app.all("/mcp", async (req, res) => {
const sid = req.header("mcp-session-id");
let transport = sid ? transports.get(sid) : undefined;
if (!transport) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => transports.set(id, transport!),
});
await server.connect(transport);
}
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
POSTは単発のJSON-RPC要求(tools/call など)を運び、応答は通常のJSONかSSEストリームで返る。GETはクライアントが長命のSSE接続を張って、Server 側からの通知(進捗イベント、notifications/tools/list_changed など)を受け取るために使う。セッション維持の責任は Mcp-Session-Id ヘッダに集約されており、ロードバランサを挟む場合は同じ Mcp-Session-Id のリクエストを同じインスタンスに吸着させるスティッキーが必要になる。
ホスティング先としては、Cloudflare Agents がドキュメント上 Streamable HTTP に対応しており、Workers 上で Durable Objects をセッション保持に使う構成が紹介されている。サーバーレス側でセッションを維持する設計に倒せるなら選択肢として有力だが、アーキテクチャ全体の詳細は公式 Cloudflare Agents ドキュメントに譲る。
セキュリティ境界
MCP は LLM の出力がツール呼び出しに直接マッピングされるプロトコルであるため、設計時にいくつかの境界を明確に引いておく必要がある。
- ユーザー承認ループ:ツール呼び出しは、原則としてユーザーが実行を許可したときだけ走るようにホストが実装する。Server 側も、破壊的操作(書き込み・送信系)には必ず
readOnlyHint: falseを意識したツール設計を行い、destructive なツールと読み取り専用ツールを分離する。 - CORS:Streamable HTTP を公開する場合、任意のオリジンからの
POSTを受け付けると CSRF 類似の問題が発生し得る。許可するオリジンは明示列挙する。ブラウザ内のクライアントを想定しない限り、CORS は閉じる側が安全側になる。 - OAuth と Bearer:
2025-03-26リビジョンで OAuth 2.1 ベースの認可フローが仕様化された。リモート Server では、Client から送られるAuthorization: Bearer ...を検証し、スコープに応じたツールのみtools/listに出すのが定石。ローカルでも、API Key を環境変数経由で渡し Server プロセス内だけで扱う。 - ローカル stdio の落とし穴:stdio だからといって安全ではない。Server プロセスは LLM が渡してくる文字列を引数に取るため、ファイルパスは必ず basedir との前方一致で検証し、シンボリックリンクを辿った先も basedir 内に収まることを確認する。環境変数をそのまま応答に混ぜないことも重要で、
process.envをログに流す実装は典型事故パターンだ。 - SSRF:HTTP Server が「URL を受け取って fetch する」ツールを公開する場合、
127.0.0.1、169.254.169.254(クラウドのメタデータエンドポイント)、::1、RFC1918 のプライベートアドレスは明示的に拒否する。DNSリバインディングを踏まえ、名前解決後のIPで最終判定するのが正しい。
ツールの実装者が守るべき最小ラインは「Server は Client を信用するが、引数は信用しない」だ。Clientは認可されたホストだがLLM由来の引数は認可された入力ではない。
次期仕様(SEP)の見通し:2026年Q2
MCP は Spec Enhancement Proposal(SEP)という形式で仕様改定案を集めており、公式発表では 2026-Q1 を提案締切、2026年6月ごろに次リビジョンの仕様公開を予定している。現行の 2025-03-26 から次版へ移るタイミングだ。
議論が活発な論点として確認されているものには、以下のような方向性がある。いずれも公式の議論ベースであり、仕様として確定したわけではない点に注意したい。
- Elicitation の拡張:Server 側から Client に対して、追加情報入力をユーザーに求める「追質問」のフローを整理する提案。現在も最小限のサポートはあるが、UI側との契約をより明確にする方向で議論されている。
- Resources の強化:大きめのリソースに対するページング・差分取得・サブスクリプションのセマンティクス整備。LLM側のコンテキストウィンドウ消費を抑えるための重要論点と見られる。
- 認可の改善:OAuth 2.1 ベースの現行認可フローを、エージェント間委譲やトークンスコープの粒度に合わせて洗練させる方向の提案が上がっている。
実装側の判断としては、現時点(2026年4月)で本番投入する Server は 2025-03-26 に合わせて書き、次リビジョン対応は SDK のマイナーアップデートに追随する方針で十分だ。SDK 側は過去リビジョンの互換レイヤを維持する前提で進んでいる。
まとめ
- MCPは JSON-RPC 2.0 の上に Tools / Resources / Prompts の3プリミティブを標準化するプロトコルで、2026年4月時点の現行リビジョンは
2025-03-26。 - トランスポートは stdio(ローカル1対1)と Streamable HTTP(リモート多重)の2本立て。SSE 単体は後方互換扱いで新規採用しない。
- TypeScript SDK では
McpServer+StdioServerTransportまたはStreamableHTTPServerTransportで、ツール定義のコードは共通化できる。 - Claude Code は
claude mcp add、Claude Desktop はclaude_desktop_config.json、Cursor は.cursor/mcp.jsonが登録窓口。 - LLM由来の引数は信頼しない。パストラバーサル、SSRF、CORS、認可スコープの4点を最低ラインで固める。
(本記事の仕様情報は2026年4月時点のModel Context Protocol公式仕様およびSDKドキュメントに基づきます。仕様は今後アップデートされる可能性があります)



