Contents
1. MCP とは何か ― 背景と現行の仕様概要
Model Context Protocol (MCP) は、AI アシスタントと外部ツール(データベース・検索エンジン・カレンダー API など)のやり取りを 統一された JSON メッセージ形式 で記述できるように設計されたインタフェースです。
- 目的
- ベンダーごとに異なるプロンプト構造やツール呼び出し方式を吸収し、クライアント側の実装負荷を低減する。
-
スキーム(JSON Schema)による入力検証で安全性・可観測性を確保する。
-
現行バージョン
- 公開されている公式リポジトリは
v1.0(2023 年リリース)です。2025 年以降に予定されているv1.2・v2.0の変更点は、現時点ではドラフト段階であり、正式リリースや仕様書の公開は確認できていません(※本稿ではあくまで「将来的な提案」レベルとして記載します)。
注記:本記事に掲載するバージョン情報は、公式サイト・GitHub リポジトリで公表された内容に基づきます。未確定の仕様(例:
v1.2/v2.0)については「将来の方向性」として扱い、実装時には必ず最新の公式ドキュメントを参照してください。
2. MCP の主要コンポーネント
| コンポーネント | 説明 | 必須フィールド |
|---|---|---|
| messages | 会話履歴(system, user, assistant)を順序通りに格納する配列。 |
role, content |
| tool_calls | AI が呼び出す外部ツール情報(名前と引数)。MCP 1.0 ではオプションですが、将来的には標準化が検討されています。 | name, arguments |
| $schemaVersion | スキーマのバージョンを明示するメタフィールド。2026 年版ドラフトで導入予定です(実装は任意)。 | 文字列 |
2‑1. メッセージ構造(TypeScript 型定義)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** 会話に出現する各ロール */ export type Role = 'system' | 'user' | 'assistant'; /** 1 件のメッセージ */ export interface Message { /** 発言者 */ role: Role; /** テキストコンテンツ(最大10 KB 推奨) */ content: string; } /** ツール呼び出し情報 */ export interface ToolCall { /** 呼び出すツールの識別子(例: "search", "get_weather") */ name: string; /** 引数は任意の JSON オブジェクト。スキーマで詳細を定義することが推奨されます。 */ arguments: Record<string, unknown>; } /** MCP のリクエスト全体(v1.0 互換) */ export interface McpRequest { /** スキーマバージョン(省略可:デフォルトは "1.0" とみなす) */ $schemaVersion?: string; /** 会話履歴 */ messages: Message[]; /** ツール呼び出し情報(ツール未使用時は空配列または undefined) */ tool_calls?: ToolCall[]; } |
ポイント:
Message.contentの長さやToolCall.argumentsの深さは、実装側で 上限を設ける ことが推奨されます。過剰なデータは JSON Schema バリデーションでエラーになるだけでなく、外部 AI API のリクエストサイズ制限に引っかかります。
3. 開発環境の構築手順
3‑1. Node.js & TypeScript のインストール
| 手順 | コマンド / 操作 |
|---|---|
| 1️⃣ Node.js(≥20)を公式サイトからダウンロードしインストール | https://nodejs.org/ja |
| 2️⃣ バージョン確認 | node -v、npm -v |
| 3️⃣ TypeScript をグローバルにインストール | bash npm install -g typescript && tsc --version |
3‑2. プロジェクト雛形の作成
|
1 2 3 4 5 6 7 8 9 10 11 |
# 1. リポジトリをクローン(公式サンプルは GitHub に公開中) git clone https://github.com/ModelContextProtocol/mcp-starter.git cd mcp-starter # 2. npm 初期化(package.json が無い場合のみ実行) npm init -y # 3. 必要パッケージのインストール npm install express @mcp/sdk dotenv # ランタイム依存 npm install --save-dev typescript ts-node jest ts-jest supertest @types/express @types/jest @types/node |
3‑3. Docker Desktop の導入(ローカルコンテナ実行用)
| OS | ダウンロード URL |
|---|---|
| Windows / macOS | https://www.docker.com/products/docker-desktop |
| Linux (Ubuntu) | sudo apt-get install docker.io (公式リポジトリ経由) |
インストール後、以下で動作確認:
|
1 2 |
docker version # CLI が表示されれば OK |
4. TypeScript で構築する MCP サーバー ― 実装ガイド
4‑1. JSON Schema バリデーションのセットアップ
公式 SDK @mcp/sdk が提供する validateMcpRequest は、draft‑2020‑12 に準拠したスキーマ検証を内部で行います。以下は型安全にラップしたユーティリティです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/validation.ts import { validateMcpRequest as rawValidate } from '@mcp/sdk'; import type { McpRequest } from './types'; export async function validateMcp(req: unknown): Promise<{ valid: true; data: McpRequest } | { valid: false; errors: string[] }> { const result = await rawValidate(req); if (result.valid) { // 型アサーションは安全(SDK がスキーマ通過を保証) return { valid: true, data: req as McpRequest }; } return { valid: false, errors: result.errors.map(e => e.message) }; } |
4‑2. Express アプリ本体
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// src/app.ts import express from 'express'; import dotenv from 'dotenv'; import { validateMcp } from './validation'; import { callOpenAiChat } from './aiProvider'; dotenv.config(); const app = express(); app.use(express.json({ limit: '1mb' })); // ペイロード上限を明示 /** POST /mcp – MCP リクエストの受信点 */ app.post('/mcp', async (req, res) => { // ---- 1️⃣ バリデーション ---- const validation = await validateMcp(req.body); if (!validation.valid) { return res.status(400).json({ error: 'Schema validation failed', details: validation.errors }); } // ---- 2️⃣ ビジネスロジック(AI 呼び出し)---- try { const aiResponse = await callOpenAiChat(validation.data); return res.json(aiResponse); } catch (err) { console.error('AI provider error:', err); // エラーは内部情報を隠蔽してクライアントに返す return res.status(502).json({ error: 'Failed to obtain AI response' }); } }); export default app; |
4‑3. OpenAI 呼び出しラッパー(エラーハンドリング強化版)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// src/aiProvider.ts import { OpenAI } from 'openai'; import type { McpRequest, Message } from './types'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // タイムアウトは SDK が内部で設定しているが、必要なら上書き可能 timeout: 30_000, }); /** * OpenAI の ChatCompletion API を呼び出すユーティリティ。 * - `tool_calls` が無い場合は通常の chat 完了を取得。 * - エラーハンドリングは HTTP ステータスと OpenAI 独自エラーコードに分岐させる。 */ export async function callOpenAiChat(request: McpRequest) { const { messages, tool_calls } = request; // メッセージ配列を SDK の型へ変換(必要なら追加フィールドは除外) const payloadMessages = messages.map<Message>((m) => ({ role: m.role, content: m.content, })); const body: any = { model: process.env.OPENAI_MODEL ?? 'gpt-4o-mini', messages: payloadMessages, }; if (tool_calls && tool_calls.length > 0) { // OpenAI の spec に合わせて `tool_calls` フィールドを付与 body.tool_calls = tool_calls; } try { const resp = await openai.chat.completions.create(body); // 成功時は最初の choice を返す(複数ある場合は要件に応じて拡張可) return resp.choices[0].message; } catch (e: any) { // OpenAI SDK が投げるエラーは `OpenAI.APIError` に準拠 if (e.status === 401) throw new Error('Invalid OpenAI API key'); if (e.status >= 500) throw new Error('OpenAI service unavailable'); throw e; // その他は再送信ロジック等でハンドリング可能 } } |
4‑4. エントリーポイント(本番実行用)
|
1 2 3 4 5 6 7 8 |
// src/index.ts import app from './app'; const PORT = Number(process.env.PORT) || 3000; app.listen(PORT, () => { console.info(`🚀 MCP server listening on http://localhost:${PORT}`); }); |
5. テスト戦略 ― 手動・自動テストのベストプラクティス
5‑1. curl / HTTP クライアントからの手動検証
|
1 2 3 4 5 6 7 8 9 |
curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{ "$schemaVersion": "1.0", "messages": [ {"role":"user","content":"今日の東京の天気は?"} ] }' |
- 期待結果:ステータス
200、レスポンスにrole: "assistant"とcontentが含まれる。 - 失敗例:
$schemaVersionを省略した場合は400でエラーメッセージが返る。
5‑2. Jest + SuperTest によるユニットテスト
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// tests/app.test.ts import request from 'supertest'; import app from '../src/app'; describe('POST /mcp エンドポイント', () => { it('スキーマエラー → 400 を返す', async () => { const res = await request(app) .post('/mcp') .send({ messages: [] }) // $schemaVersion が欠如 .set('Content-Type', 'application/json'); expect(res.status).toBe(400); expect(res.body.error).toContain('Schema validation failed'); }); it('有効なリクエスト → OpenAI からの応答を取得できる (モック)', async () => { // callOpenAiChat をモック化 jest.spyOn(require('../src/aiProvider'), 'callOpenAiChat') .mockResolvedValue({ role: 'assistant', content: '晴れです。' }); const res = await request(app) .post('/mcp') .send({ "$schemaVersion": "1.0", "messages": [{ "role": "user", "content": "天気は?" }] }) .set('Content-Type', 'application/json'); expect(res.status).toBe(200); expect(res.body.role).toBe('assistant'); expect(res.body.content).toBe('晴れです。'); }); }); |
ポイント
- テストは外部 AI API に依存しないよう必ずモック化する。
- バリデーションロジックの網羅的テスト(必須項目欠如、型不一致、サイズ超過)を追加すると安心です。
5‑3. デバッグ時に役立つチェックリスト
| 症状 | 原因例 | 確認手順 |
|---|---|---|
| 400 Bad Request – スキーマエラー | $schemaVersion 欠如、messages が配列でない |
console.debug(JSON.stringify(req.body, null, 2)) → JSON Schema Validator(オンライン)で検証 |
| 401 Unauthorized – API キー不正 | 環境変数未設定・キーに余計な空白 | node -e "console.log('key:', process.env.OPENAI_API_KEY)" |
| 504 Gateway Timeout | 外部 AI の応答遅延 | SDK の timeout 設定を 30 s に上げ、リトライロジック(axios-retry 等)を実装 |
| 500 Internal Server Error – 未捕捉例外 | コード内で throw したままキャッチしていない |
try/catch 範囲を広げ、エラーログにスタックトレースを出力 |
6. Docker 化とクラウドデプロイ ― 本番環境への移行手順
6‑1. マルチステージビルドの Dockerfile
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# ---------- ビルドステージ ---------- FROM node:20-alpine AS builder WORKDIR /app # パッケージ情報だけを先にコピーしてキャッシュを有効化 COPY package*.json ./ RUN npm ci --omit=dev # 本番依存のみインストール # ソースコードと TypeScript 設定をコピーしコンパイル COPY tsconfig.json . COPY src ./src RUN npx tsc --project tsconfig.json # ---------- 実行ステージ ---------- FROM node:20-alpine WORKDIR /app ENV NODE_ENV=production # ビルド成果物だけをコピー COPY --from=builder /app/dist ./dist COPY package*.json ./ RUN npm ci --omit=dev && rm -rf src test # 本番実行に不要なファイルは削除 # 非 root ユーザーで実行(セキュリティベストプラクティス) USER node EXPOSE 3000 CMD ["node", "dist/index.js"] |
ビルド・起動コマンド
|
1 2 3 4 5 6 |
docker build -t mcp-server:latest . docker run -d -p 3000:3000 --name mcp-prod \ -e OPENAI_API_KEY=**** \ -e PORT=3000 \ mcp-server:latest |
6‑2. 主なクラウドプラットフォームへのデプロイ手順
| プラットフォーム | 手順概要 |
|---|---|
| AWS (ECS/Fargate) | 1️⃣ ECR リポジトリ作成 → docker tag → docker push。2️⃣ ECS タスク定義でコンテナイメージと環境変数(Secrets Manager 推奨)を設定。 3️⃣ ALB で HTTPS 終端し、ターゲットポート 3000 に転送。 |
| Google Cloud Run | gcloud builds submit --tag gcr.io/<PROJECT>/mcp-server → Cloud Run デプロイ時に「認証なし」または IAM ベースのアクセス制御を選択。シークレットマネージャーで API キーを注入し、HTTPS が自動的に有効化されます。 |
| Azure Container Apps | ACR にプッシュ → Container Apps で「Ingress: External」設定、環境変数は Azure Key Vault 経由で渡す。 |
セキュリティチェックリスト(共通)
- API キーの管理
- 環境変数ではなく シークレットマネージャ → コンテナ起動時に注入。
-
定期的なローテーションと最小権限設定を徹底。
-
TLS / HTTPS
-
本番は必ずロードバランサ/クラウドサービスが提供する TLS 終端を利用。コンテナ内部は HTTP のみで OK。
-
入力サイズ制限
-
express.json({ limit: '1mb' })に加えて、スキーマ側でもmaxLength/maxItemsを設定。 -
ロギング & モニタリング
- 標準出力に構造化 JSON ログを出す → CloudWatch Logs / GCP Logging が自動収集。
-
エラーレート・レイテンシは Prometheus メトリクスまたは各クラウドのメトリックサービスでアラート設定。
-
最小権限実行ユーザー
- Dockerfile の
USER node(UID 1000)により root 権限を排除。 - 必要なファイルシステムは読み取り専用マウント (
--read-only) を検討。
7. 将来的な拡張方向(※公式未確定情報)
| 想定される拡張 | 現在の課題 | 実装上の留意点 |
|---|---|---|
スキーマバージョニング ($schemaVersion) の必須化 |
バックワード互換性が曖昧になる恐れ | 旧バージョンを受け入れるラッパー層を設置し、v1.x と v2.0 を自動変換 |
| tool_calls の標準化(名前空間、認証情報の埋め込み) | 各ベンダーが独自実装で相互運用性が低下 | JSON Schema で「tool‑registry」参照を必須にし、中央管理サーバで検証 |
| ストリーミング応答(SSE / WebSocket) | 現行は単発レスポンスのみ | Express の res.write と SSE ヘッダー (Content-Type: text/event-stream) を組み合わせる実装を別モジュール化 |
これらは 公式リリースが確定した段階で 再度コードベースに取り込むことを推奨します。現時点では 互換性の高い v1.0 に準拠した実装で十分です。
8. まとめ
- MCP は JSON Schema に基づく型安全なインタフェース であり、AI とツール呼び出しを統一的に扱える点が最大の魅力です。
- 本稿では 公式 v1.0 スペック をベースに、実装上必要な型定義・バリデーション・エラーハンドリングを具体例として示しました。
- 開発フローは「ローカル環境 → Docker コンテナ化 → クラウドデプロイ」の三段階で構築し、シークレット管理・TLS・非 root 実行 といったセキュリティベストプラクティスを必ず組み込んでください。
- 未確定の将来仕様(v1.2 / v2.0)については 公式情報が公開されたら再評価 し、必要に応じてコードやデプロイ設定を更新します。
次のステップ:リポジトリをクローンしたら
npm run build && npm startでローカルサーバを起動し、上記 curl コマンドで動作確認。その後 Docker イメージ化 → お好みのクラウドへデプロイ、と段階的に進めるとスムーズです。
本稿の情報は執筆時点(2026 年 5 月)における公式リソースを基に作成しています。実装前には必ず最新版の MCP ドキュメントをご確認ください。