生成AIを「賢いチャット」から「実際に業務を動かすシステム」に変える鍵は、モデルを外部のツール・データ・業務ロジックに安全に繋ぐことです。その接続を標準化したのが MCP(Model Context Protocol) ——AIモデルとツールの間の“共通端子”です。ツールごとに独自連携を作らず、1つのプロトコルで繋ぐ。だからこそ2025年以降、主要なAIクライアントとプラットフォームが一斉に採用しました。
しかしMCPサーバーを本番に載せるときの勝負どころは、プロトコルの物珍しさではありません。 「アーキテクチャの理解」「トランスポートの選択」「OAuth 2.1認可」、そして何より「ツール=任意コード実行という前提に立ったセキュリティ設計」です。この記事は、現行の安定仕様(2025-11-25)と公式TypeScript SDK(v1系)に厳密準拠し、本番で効く実装だけを実コードで示します。
本記事のバージョン・API・セキュリティ要件は、すべて公式仕様(
modelcontextprotocol.io)で確認した一次情報に基づきます。仕様は日付リビジョンで更新されるため、実装前に必ず出典を確認してください。
MCPの全体像:host / client / server と3つのプリミティブ
MCPは JSON-RPC 2.0 をベースにした、ステートフルな接続プロトコルです。登場人物は3者。
| 役割 | 責務(公式定義の要約) |
|---|---|
| Host | コンテナ兼コーディネータ。複数のクライアントを生成・管理し、接続許可とライフサイクル、セキュリティポリシーと同意(ユーザー認可)を強制、LLM連携とコンテキスト集約を担う |
| Client | Hostが生成し、1つのサーバーと1:1のステートフルなセッションを張る。プロトコル交渉と双方向メッセージのルーティングを担当 |
| Server | 特化した機能を提供。Tools / Resources / Prompts を公開し、独立して動作。セキュリティ制約を尊重しなければならない |
サーバーが公開する3つのプリミティブ(ここが実装の中心):
- Tools(ツール) — 「AIモデルが実行する関数」。副作用を伴う操作の入口。最も注意が必要。
- Resources(リソース) — 「ユーザーやAIが使うコンテキスト・データ」。ファイルやDBレコードなど。
- Prompts(プロンプト) — 「ユーザー向けのテンプレート化されたメッセージ/ワークフロー」。
さらにクライアント側が提供する機能(サーバーから要求できる)として Sampling(サーバー起点のLLM呼び出し)・Roots(操作範囲のURI/ファイル境界)・Elicitation(ユーザーへの追加入力要求) があります。どの機能が使えるかは、初期化時の能力ネゴシエーション(capability negotiation) で双方が明示的に宣言して決まります。
トランスポートの選択:stdio か Streamable HTTP か
MCPの標準トランスポートは2つだけです。用途で選びます。
| トランスポート | いつ使う | 要点 |
|---|---|---|
| stdio | ローカル。クライアントがサーバーをサブプロセスとして起動 | JSON-RPCをstdin/stdoutで改行区切り送受信(メッセージに改行を含めない)。stderrはログ専用。クライアントは可能な限りstdioを優先すべき |
| Streamable HTTP | リモート。独立プロセスが複数接続をHTTPで処理 | POST/GETの単一エンドポイント。必要に応じSSEでストリーム。認証・Origin検証が必須級 |
⚠️ 旧 HTTP+SSE トランスポート(2024-11-05版)は非推奨で、Streamable HTTP に置き換えられました。新規実装で選ぶ理由はありません。
Streamable HTTP のセキュリティは“任意”ではありません(公式のMUST/SHOULD):
- サーバーは全接続で
Originヘッダを検証し、不正なら 403 を返す(DNSリバインディング対策)。 - ローカル実行時は
0.0.0.0ではなく127.0.0.1にバインドする。 - セッションIDは
MCP-Session-Idヘッダで扱い、**暗号的に安全(安全に生成したUUID等)**でなければならない。 - HTTPクライアントは全リクエストに
MCP-Protocol-Versionを付ける。
// Streamable HTTP: DNS リバインディング対策として Origin を必ず検証する(公式MUST)
const ALLOWED_ORIGINS = new Set(["https://app.example.com"]);
function assertTrustedOrigin(req: Request): void {
const origin = req.headers.get("origin");
// Origin が付く(=ブラウザ由来)のに許可リストに無ければ拒否
if (origin && !ALLOWED_ORIGINS.has(origin)) {
throw new HttpError(403, "forbidden origin");
}
}
// セッションIDは推測不能に。認証をセッションに依存させない(後述の session hijacking 対策)。
const sessionId = crypto.randomUUID();
最小の本番MCPサーバー(公式SDK v1・TypeScript)
まず“動く最小形”です。本番は安定版 @modelcontextprotocol/sdk(v1系) を使います(@modelcontextprotocol/server v2 はベータでAPIが異なるため、本番採用は時期尚早)。ツールは server.registerTool で登録し、inputSchema には Zodの「生シェイプ」(z.object(...) で包まないオブジェクト)を渡すのが v1 の作法です。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "acme-tools", version: "1.0.0" });
// inputSchema は「生の Zod シェイプ」。境界で入力を型と値域で締める。
server.registerTool(
"search-orders",
{
title: "注文検索",
description: "テナント内の注文を検索する(読み取り専用)",
inputSchema: { tenantId: z.string().uuid(), query: z.string().min(1).max(200) },
},
async ({ tenantId, query }) => {
// 認可は「MCPの外」の決定的コードで強制する。プロンプトに委ねない。
const orders = await searchOrders(tenantId, query);
return { content: [{ type: "text", text: JSON.stringify(orders) }] };
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
registerTool(name, config, handler)・生シェイプのinputSchema・{ content: [{ type: "text", text }] } の戻り値・server.connect(transport) は、いずれもv1.29.0の公式ドキュメントで確認した現行APIです。構造化出力が必要なら outputSchema と structuredContent を併用できます。LLM出力の検証設計は structured output の信頼性設計 と同じ発想です。
ツールを“安全に”設計する:任意コード実行という前提
MCPで最も誤解されるのが Tools です。公式仕様は明言します——「ツールは任意コード実行を表し、相応の注意をもって扱わねばならない」。つまり、MCPツールを開けることは、LLM(と、それを操作しうる攻撃者)に、あなたのシステムの関数を呼ばせることです。
だから設計原則は、LLMアプリのセキュリティ(OWASP LLM06「過剰なエージェンシー」)と完全に地続きです——破壊的な操作は、ツールに「実行」させず「提案」だけを返させる。
// ツール = 任意コード実行。破壊的操作は「提案」を返し、その場では実行しない。
server.registerTool(
"propose-refund",
{
title: "返金の提案",
description: "返金の提案レコードを作成する(実行はしない・人間承認が必要)",
inputSchema: { orderId: z.string().uuid(), amountJpy: z.number().int().positive() },
},
async ({ orderId, amountJpy }) => {
// 副作用は「提案の作成」のみ(最小権限)。実際の返金は承認済みのコード経路だけ。
const proposalId = await createRefundProposal({ orderId, amountJpy });
return {
content: [{ type: "text", text: `提案を作成しました: ${proposalId}(承認待ち)` }],
};
},
);
実際の返金は、MCPの外側で、決定的な業務ルール検証+人間承認+冪等キーを通して行います。この「提案と実行の分離+冪等性」は、私が決済基盤で本番二重課金0件を保つために使ってきた信頼性・冪等性の設計そのものです。加えて公式は、**「ツールの説明(annotation)は、信頼できるサーバー由来でない限り信頼しない」**とも述べています。つまりツールのメタデータ自体が攻撃面になり得ます。ツールの並べ方・権限設計は AIエージェントのツール実行設計 も参照してください。
リモートMCPの認可:OAuth 2.1(token passthrough は禁止)
認可はHTTPトランスポートに限った任意機能です(stdioは環境から資格情報を取得するため、通常は認可を使いません)。リモートMCPを公開するなら、公式の認可モデルに厳密に従います。
- MCPサーバー = OAuth 2.1 の「リソースサーバー」、MCPクライアント = OAuthクライアント、別に認可サーバー(AS)が存在。
- RFC 9728(Protected Resource Metadata) をサーバーは実装必須。401時の
WWW-Authenticateか/.well-known/oauth-protected-resourceでASを発見させる。 - RFC 8707(Resource Indicators) — クライアントは認可・トークン両リクエストで、MCPサーバーの正規URIを
resourceに必ず送る。 - PKCE(S256)必須。ASのメタデータで対応を確認できなければ続行しない。
- トークンの検証は妥協できない(公式のMUST):
// リモートMCP = OAuth 2.1 の resource server。
// 受け取ったトークンは「自分向け(aud)」だけ受理する。
function verifyAccessToken(rawToken: string): Claims {
const claims = verifyJwt(rawToken, JWKS); // 署名・iss・exp を検証
// aud に自分の正規URIが無いトークンは拒否(他サービス向けの流用を防ぐ)
if (!claims.aud?.includes(MCP_SERVER_CANONICAL_URI)) {
throw new HttpError(401, "token audience mismatch");
}
return claims;
// ★ このトークンを下流の外部APIへ「素通し」してはならない(公式で明確に禁止)。
// 下流呼び出しは、MCPサーバー自身の資格情報/別トークンで行う。
}
**token passthrough(受領トークンをそのまま下流へ横流しする)は、認可仕様で明確に禁止された“アンチパターン”**です。監査証跡を壊し、信頼境界を越えさせ、セキュリティ制御を回避させます。
本番セキュリティ設計:公式ベストプラクティスの脅威と対策
公式の Security Best Practices が名指しする代表的な脅威と、対策の要点です。
| 脅威 | 何が起きる | 対策の要点 |
|---|---|---|
| Confused Deputy | プロキシが静的client_id+動的登録+同意Cookieを持つと、Cookie再利用で同意を飛ばして認可コードを窃取 | プロキシはクライアントごとの同意(per-client consent)を必須実装。承認済みclient_idを都度確認、redirect_uriは完全一致 |
| Token Passthrough | 自分向けでないトークンを受理し下流へ素通し | 自分向け以外のトークンを受理しない。仕様で明確に禁止 |
| Session Hijacking | セッションIDの推測・なりすまし | セッションを認証に使わない。推測不能なセッションID(安全なUUID)。全受信リクエストを検証。IDはユーザーに束ねる |
| SSRF | 発見用フィールドに内部URL(例:クラウドメタデータ169.254.169.254)を仕込む | HTTPSを強制、プライベートIP帯をブロック、リダイレクト先を検証(自作IP判定は符号化バイパスに注意) |
| ローカルサーバー起動 | 悪意ある起動コマンドで任意コード実行 | クライアントは起動コマンドを明示し明示的同意を要求、サンドボックス化 |
| 過剰スコープ | admin:* 等の広すぎるスコープで被害が拡大 | 最小権限のスコープを段階的に。ワイルドカード/万能スコープを避ける |
これらは特別な話ではなく、**「MCPサーバーを、信頼できないクライアント(と、その先のLLM)に晒される普通のWeb APIとして防御する」**ことに尽きます。
可観測性・回復性・冪等性:本番運用の三点
MCPツールは外部システムを叩くため、可観測性が欠かせません。ツール呼び出し名・引数(PIIはマスク)・レイテンシ・成否・拒否をユーザー/セッション単位で記録し、異常を検知します(土台は OpenTelemetryによる本番可観測性)。加えて、外部呼び出しにはタイムアウトとリトライ(指数バックオフ)、副作用のある操作には冪等キーを必ず入れます。LLMは同じ意図でツールを複数回呼ぶことがあり、冪等でない実行は二重課金・二重発送に直結します。
バージョニングと将来への備え
MCPの版は日付ベース(後方非互換な変更があった日付)。現行の安定版は 2025-11-25、2026-07-28 はリリース候補(RC)で未確定です。SDKも過渡期で、安定版 @modelcontextprotocol/sdk(v1)と、APIの異なるベータ @modelcontextprotocol/server(v2)が並存します。本番はv1に固定し、MCP-Protocol-Version によるネゴシエーションで版差を吸収、破壊的変更に備えます。
導入チェックリスト(本番公開前)
- トランスポート — ローカルはstdio、リモートはStreamable HTTP(旧HTTP+SSEは使わない)。
- Origin検証 — Streamable HTTPで
Originを検証し不正は403。ローカルは127.0.0.1バインド。 - セッション — 推測不能なセッションID。認証をセッションに依存させない。全受信を検証。
- 認可(リモート) — OAuth 2.1、RFC 9728/8707、PKCE(S256)。トークンのaudを検証し自分向けのみ受理。
- token passthrough禁止 — 受領トークンを下流へ横流ししない。下流は別資格情報で。
- ツール最小権限 — 破壊的操作は「提案」に留め、実行は人間承認+冪等キー付きのコード経路で。
- 入力検証 —
inputSchema(Zod)で型・値域を締め、認可は決定的コードで再確認。 - 可観測性 — ツール呼び出しのログ/トレース、タイムアウト、リトライ、異常検知。
- SDK/仕様の固定 — v1安定SDK、仕様版を明示、更新は一次情報で追う。
まとめ
MCPは、AIを業務システムに繋ぐための強力な標準です。しかし本番の成否を分けるのは、プロトコルの新しさではなく——「ツール=任意コード実行」「LLM=確率的で信頼できないコンポーネント」という前提に立ち、その周りを決定的なアーキテクチャで囲えるかです。
- アーキテクチャ:host/client/server と3プリミティブ、能力ネゴシエーションを理解する。
- トランスポート:stdio(ローカル)/ Streamable HTTP(リモート)を用途で選び、Origin・セッションを守る。
- 認可:OAuth 2.1のリソースサーバーとして、audを検証し token passthrough を禁じる。
- ツール安全性:最小権限・提案/実行分離・人間承認・冪等性で、被害半径を封じ込める。
MCPサーバーのセキュリティは、LLMアプリのセキュリティの一部です。「速く繋ぐ」ことと「安全に運用する」ことは、正しく設計すれば両立します。