# MCP（Model Context Protocol）本番サーバー実装ガイド 2026｜アーキテクチャ・トランスポート・認可・セキュリティ

> Model Context Protocol（MCP）で本番に耐えるサーバーを実装するための実践ガイド。host/client/serverアーキテクチャ、stdio/Streamable HTTPトランスポート、公式TypeScript SDK（v1）の正しいAPI、OAuth 2.1認可（RFC 9728/8707・PKCE・トークンaudience検証）、ツール安全設計と公式セキュリティベストプラクティスまで、一次情報準拠のTypeScript実コードで解説します。

- 公開日: 2026-07-23
- 著者: 友田 陽大
- タグ: MCP, Model Context Protocol, 生成AI, LLM, AIエージェント, TypeScript, セキュリティ, OAuth, アーキテクチャ設計, 技術選定
- URL: https://tomodahinata.com/blog/mcp-model-context-protocol-production-server-guide
- カテゴリ: 本番LLMアプリ実装（信頼できるAI）
- 総合ガイド: https://tomodahinata.com/blog/production-llm-application-engineering-guide

## 要点

- MCPはAIモデルを外部のツール・データ・プロンプトに繋ぐオープン標準（JSON-RPC 2.0）。host/client/serverの3者構成で、サーバーはTools/Resources/Promptsを公開する。
- トランスポートは stdio（ローカル・サブプロセス）と Streamable HTTP（リモート）の2つ。旧HTTP+SSEは非推奨に置き換えられた。
- 本番は安定版SDK @modelcontextprotocol/sdk（v1系）を使い、server.registerTool にZodの生シェイプを渡す。@modelcontextprotocol/server（v2）はベータでAPIが異なる。
- リモートMCPはOAuth 2.1のresource server。受領トークンはaud（audience）を検証して自分向けのみ受理し、下流へ素通し（token passthrough）してはならない——公式で明確に禁止されている。
- ツールは『任意コード実行』。最小権限・破壊的操作の提案/実行分離・人間承認・Origin検証で、プロンプトインジェクションの被害半径を封じ込める。

---

生成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`** を付ける。

```ts
// 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 の作法です。

```ts
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 の信頼性設計](/blog/structured-output-reliability-constrained-decoding-semantic-validation) と同じ発想です。

## ツールを“安全に”設計する：任意コード実行という前提

MCPで最も誤解されるのが Tools です。公式仕様は明言します——**「ツールは任意コード実行を表し、相応の注意をもって扱わねばならない」**。つまり、MCPツールを開けることは、**LLM（と、それを操作しうる攻撃者）に、あなたのシステムの関数を呼ばせる**ことです。

だから設計原則は、[LLMアプリのセキュリティ](/blog/llm-application-security-owasp-top-10-prompt-injection-guide)（OWASP LLM06「過剰なエージェンシー」）と完全に地続きです——**破壊的な操作は、ツールに「実行」させず「提案」だけを返させる**。

```ts
// ツール = 任意コード実行。破壊的操作は「提案」を返し、その場では実行しない。
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件を保つために使ってきた[信頼性・冪等性の設計](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)そのものです。加えて公式は、**「ツールの説明（annotation）は、信頼できるサーバー由来でない限り信頼しない」**とも述べています。つまりツールのメタデータ自体が攻撃面になり得ます。ツールの並べ方・権限設計は [AIエージェントのツール実行設計](/blog/ai-agent-tool-use-function-calling-production-design) も参照してください。

## リモート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）:

```ts
// リモート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による本番可観測性](/blog/opentelemetry-observability-production-tracing-metrics-logs)）。加えて、外部呼び出しには**タイムアウトとリトライ（指数バックオフ）**、副作用のある操作には**冪等キー**を必ず入れます。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アプリのセキュリティ](/blog/llm-application-security-owasp-top-10-prompt-injection-guide)の一部です。「速く繋ぐ」ことと「安全に運用する」ことは、正しく設計すれば両立します。
