はじめに
Model Context Protocol (MCP) は、AIアシスタントが外部ツールやデータソースと連携するための標準プロトコルです。本記事では、MCPサーバーの3つの主要なトランスポート方式(STDIO、SSE、Streamable HTTP)について、実際のTypeScript実装を交えながら詳しく解説します。
このガイドで使用している全てのコードは、GitHubで公開されています。
💡 サンプルアプリケーションについて
本記事で実装するMCPサーバーは、OpenAI APIをツールとして提供するシンプルなアプリケーションです。
機能概要:
- Claude DesktopなどのMCPクライアントから、OpenAIのGPTモデルに質問を投げられる
-
ask_openai
ツールを通じて、任意のプロンプトをOpenAI APIに送信 - GPT-4、GPT-4o-mini など、使用するモデルを選択可能
- API呼び出しのエラーを適切にハンドリング
実用例:
// MCPクライアント(Claude)から呼び出し
const result = await callTool("ask_openai", {
prompt: "TypeScriptの型システムについて簡潔に説明してください",
model: "gpt-4o-mini"
});
// → OpenAI APIから回答を取得してクライアントに返却
この同じアプリケーションを、3つの異なるトランスポート方式で実装することで、それぞれの特徴と使い分けを理解できます。
目次
MCPの基本概念
MCP (Model Context Protocol) は、以下の要素で構成されています:
- サーバー: ツールやリソースを提供する側
- クライアント: AIアシスタント(Claudeなど)
- トランスポート: 通信方式(STDIO / SSE / HTTP)
- JSON-RPC: メッセージフォーマット
今回の実装例では、OpenAI APIを呼び出す ask_openai
ツールを提供するシンプルなMCPサーバーを3つの方式で実装します。
3つのトランスポート方式の概要
特徴 | STDIO | SSE | Streamable HTTP |
---|---|---|---|
通信方式 | 標準入出力 | Server-Sent Events | HTTP POST |
接続形態 | プロセス間通信 | ロングポーリング | リクエスト/レスポンス |
ネットワーク | ローカルのみ | HTTP経由 | HTTP経由 |
複雑さ | 低 | 中 | 低〜中 |
リアルタイム性 | 高 | 高 | 中 |
スケーラビリティ | 低 | 中 | 高 |
STDIO実装の詳細
概要
STDIOトランスポートは、標準入出力(stdin/stdout)を使用してクライアントとサーバー間で通信します。最もシンプルで、ローカル環境での使用に最適です。
アーキテクチャ
┌─────────────┐ stdin/stdout ┌─────────────┐
│ Claude │ ◄──────────────────────────► │ MCP Server │
│ (Client) │ JSON-RPC Messages │ (STDIO) │
└─────────────┘ └─────────────┘
実装のポイント
1. サーバーの初期化
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "mcp-stdio-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
ポイント:
-
Server
クラスでMCPサーバーのコア機能を実装 -
capabilities
でサーバーが提供する機能を宣言 - この例では
tools
機能(ツール実行)を提供
2. ツールの登録
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "ask_openai",
description: "OpenAI APIを使用して質問に回答します",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "OpenAIに送信する質問やプロンプト",
},
model: {
type: "string",
description: "使用するOpenAIモデル(デフォルト: gpt-4o-mini)",
default: "gpt-4o-mini",
},
},
required: ["prompt"],
},
},
],
};
});
ポイント:
-
ListToolsRequestSchema
は、クライアントが利用可能なツール一覧を取得するためのスキーマ -
inputSchema
でツールの入力パラメータをJSON Schemaで定義 - クライアントはこの情報を基にツールを呼び出す
3. ツール実行の処理
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "ask_openai") {
const { prompt, model = "gpt-4o-mini" } = request.params.arguments as {
prompt: string;
model?: string;
};
try {
const completion = await openai.chat.completions.create({
model: model,
messages: [{ role: "user", content: prompt }],
});
const response = completion.choices[0]?.message?.content || "回答を取得できませんでした";
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
content: [{ type: "text", text: `エラーが発生しました: ${errorMessage}` }],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
ポイント:
-
CallToolRequestSchema
でツール実行リクエストを処理 - OpenAI APIを呼び出して結果を返す
- エラーハンドリングを適切に実装
4. トランスポートの接続
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP STDIO Server running...");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
ポイント:
-
StdioServerTransport
でstdin/stdoutを使用 -
console.error
を使用(stdoutはMCP通信に使用されるため) - エラーハンドリングでプロセスを適切に終了
Claude Desktopでの設定
{
"mcpServers": {
"stdio-openai": {
"command": "node",
"args": ["/path/to/mcp-stdio/dist/index.js"],
"env": {
"OPENAI_API_KEY": "your_openai_api_key_here"
}
}
}
}
メリット・デメリット
メリット:
- ✅ 実装が最もシンプル
- ✅ オーバーヘッドが少ない
- ✅ レイテンシーが低い
- ✅ セットアップが簡単
デメリット:
- ❌ ローカル環境でのみ動作
- ❌ ネットワーク越しの通信不可
- ❌ 複数クライアントからの同時接続不可
- ❌ プロセス管理が必要
SSE実装の詳細
概要
Server-Sent Events (SSE) は、サーバーからクライアントへの一方向ストリーミングを実現するHTTP技術です。WebSocketに比べてシンプルで、HTTP上で動作するため、ファイアウォールやプロキシとの互換性が高いです。
アーキテクチャ
┌─────────────┐ GET /sse (EventStream) ┌─────────────┐
│ Claude │ ◄─────────────────────────── │ Express │
│ (Client) │ │ Server │
│ │ ──────────────────────────► │ │
└─────────────┘ POST /message (JSON-RPC) └─────────────┘
実装のポイント
1. Expressサーバーのセットアップ
import express from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const app = express();
app.use(express.json());
const server = new Server(
{
name: "mcp-sse-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
ポイント:
- Expressを使用してHTTPサーバーを構築
-
express.json()
でJSON形式のリクエストボディをパース - MCPサーバーの初期化はSTDIOと同様
2. SSEエンドポイントの実装
app.get("/sse", async (req, res) => {
console.log("New SSE connection established");
const transport = new SSEServerTransport("/message", res);
await server.connect(transport);
// クライアントが接続を閉じた時のクリーンアップ
req.on("close", () => {
console.log("SSE connection closed");
});
});
ポイント:
-
GET /sse
エンドポイントでSSE接続を確立 -
SSEServerTransport
にレスポンスオブジェクトを渡す - メッセージ送信用のパス(
/message
)を指定 - クライアント切断時のクリーンアップを実装
3. メッセージ受信エンドポイント
app.post("/message", async (req, res) => {
console.log("Received message:", req.body);
// SSEServerTransportが自動的にメッセージを処理します
res.sendStatus(200);
});
ポイント:
- クライアントからのJSON-RPCメッセージを受信
-
SSEServerTransport
が内部的にメッセージを処理 - 200ステータスを返して受信を確認
4. ヘルスチェックとサーバー起動
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`MCP SSE Server running on http://localhost:${PORT}`);
console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
console.log(`Message endpoint: http://localhost:${PORT}/message`);
});
ポイント:
-
/health
エンドポイントでサーバーの健全性を確認可能 - ポート番号は環境変数で設定可能
Claude Desktopでの設定
{
"mcpServers": {
"sse-openai": {
"url": "http://localhost:3000/sse",
"env": {
"OPENAI_API_KEY": "your_openai_api_key_here"
}
}
}
}
メリット・デメリット
メリット:
- ✅ HTTP上で動作(ファイアウォールフレンドリー)
- ✅ サーバーからのプッシュ通知が可能
- ✅ ネットワーク越しの通信が可能
- ✅ WebSocketより実装がシンプル
- ✅ 自動再接続が可能
デメリット:
- ❌ STDIOより実装が複雑
- ❌ HTTP接続のオーバーヘッドがある
- ❌ 双方向通信には2つのチャネルが必要
- ❌ 接続維持にリソースを消費
Streamable HTTP実装の詳細
概要
Streamable HTTPは、標準的なHTTP POST リクエスト/レスポンスモデルを使用します。最も汎用的で、既存のHTTPインフラストラクチャとの統合が容易です。
アーキテクチャ
┌─────────────┐ POST /mcp (JSON-RPC) ┌─────────────┐
│ Claude │ ──────────────────────────► │ Express │
│ (Client) │ │ Server │
│ │ ◄─────────────────────────── │ │
└─────────────┘ Response (JSON-RPC) └─────────────┘
実装のポイント
1. サーバーセットアップ
import express, { Request, Response } from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
const app = express();
app.use(express.json());
const server = new Server(
{
name: "mcp-streamable-http-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
ポイント:
- Expressベースの実装
- JSON-RPCメッセージ型を明示的にインポート
2. MCPエンドポイントの実装
app.post("/mcp", async (req: Request, res: Response) => {
const message = req.body as JSONRPCMessage;
console.log("Received request:", message);
try {
// Content-Typeをストリーミング用に設定
res.setHeader("Content-Type", "application/json");
res.setHeader("Transfer-Encoding", "chunked");
// リクエストを処理
const response = await server.handleRequest(message);
// レスポンスを送信
res.write(JSON.stringify(response));
res.end();
console.log("Sent response:", response);
} catch (error) {
console.error("Error handling request:", error);
const errorResponse = {
jsonrpc: "2.0",
id: "id" in message ? message.id : null,
error: {
code: -32603,
message: error instanceof Error ? error.message : "Internal error",
},
};
res.write(JSON.stringify(errorResponse));
res.end();
}
});
ポイント:
-
POST /mcp
で全てのJSON-RPCリクエストを受け付け -
Transfer-Encoding: chunked
でストリーミングレスポンスを実現 -
server.handleRequest()
でMCPサーバーがリクエストを自動処理 - JSON-RPC標準のエラーレスポンスを実装
3. curlでのテスト例
# ツール一覧の取得
curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
# ツールの実行
curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "ask_openai",
"arguments": {
"prompt": "Hello, how are you?"
}
}
}'
Claude Desktopでの設定
{
"mcpServers": {
"http-openai": {
"url": "http://localhost:3001/mcp",
"transportType": "http",
"env": {
"OPENAI_API_KEY": "your_openai_api_key_here"
}
}
}
}
メリット・デメリット
メリット:
- ✅ 最も汎用的(どんなHTTPクライアントからも利用可能)
- ✅ ステートレスで水平スケールが容易
- ✅ ロードバランサーとの親和性が高い
- ✅ 既存のHTTPインフラを活用できる
- ✅ デバッグが容易(curlで簡単にテスト可能)
- ✅ REST APIライクで理解しやすい
デメリット:
- ❌ リアルタイム性がSSEより低い
- ❌ 各リクエストで接続を確立(オーバーヘッド)
- ❌ サーバープッシュには向かない
- ❌ 長時間実行される処理には不向き
徹底比較
1. 通信フロー比較
STDIO
Client Process → stdin → Server Process
Server Process → stdout → Client Process
- プロセス間通信(IPC)
- 双方向通信が一つのチャネルで完結
- メッセージは順序保証される
SSE
Client → GET /sse → Server (接続維持)
Server → EventStream → Client (サーバープッシュ)
Client → POST /message → Server (クライアントリクエスト)
- 2つの独立したHTTP接続
- サーバーからのプッシュとクライアントからのリクエストが別々
- 接続が維持される
Streamable HTTP
Client → POST /mcp → Server
Server → Response → Client
- リクエスト/レスポンスモデル
- 各リクエストで新しい接続
- ステートレス
2. パフォーマンス比較
指標 | STDIO | SSE | Streamable HTTP |
---|---|---|---|
レイテンシー | 最小(~1ms) | 低(~10-50ms) | 中(~50-100ms) |
スループット | 高 | 中 | 中 |
メモリ使用量 | 低 | 中(接続維持) | 低 |
CPU使用量 | 低 | 中 | 低〜中 |
同時接続数 | 1 | 多(制限あり) | 多(制限なし) |
3. ユースケース比較
STDIO が適している場合
- ✅ Claude Desktopなどのローカルアプリケーション
- ✅ 開発環境でのテスト
- ✅ シンプルな個人プロジェクト
- ✅ 低レイテンシーが重要な場合
- ✅ ネットワークアクセスが不要な場合
SSE が適している場合
- ✅ Webアプリケーションとの統合
- ✅ リアルタイムアップデートが必要な場合
- ✅ サーバーからのプッシュ通知が必要
- ✅ 長時間接続を維持したい場合
- ✅ WebSocketほどの双方向性は不要な場合
Streamable HTTP が適している場合
- ✅ マイクロサービスアーキテクチャ
- ✅ 高いスケーラビリティが必要
- ✅ ロードバランサーを使用する場合
- ✅ 既存のHTTPインフラを活用したい
- ✅ ステートレスな設計が好ましい場合
- ✅ 様々なクライアントからアクセスしたい
4. コード量の比較
STDIO: 約120行(最小)
SSE: 約150行(中程度)
Streamable HTTP:約140行(中程度)
5. デプロイの複雑さ
STDIO
1. ビルド: tsc
2. 実行: node dist/index.js
- 最もシンプル
- デプロイツール不要
SSE
1. ビルド: tsc
2. 実行: node dist/index.js
3. ポート開放(3000番など)
4. 必要に応じてリバースプロキシ設定
- HTTPサーバーの管理が必要
- ファイアウォール設定が必要
Streamable HTTP
1. ビルド: tsc
2. 実行: node dist/index.js
3. ポート開放(3001番など)
4. ロードバランサー設定(オプション)
5. コンテナ化(オプション)
- 本番環境ではより多くの設定が必要
- スケーリングのためのインフラが必要
6. エラーハンドリングとリトライ
STDIO
- プロセスクラッシュ時はクライアントが再起動
- リトライはクライアント側で実装
- デバッグは標準エラー出力で
SSE
- 接続切断時の自動再接続機能
- タイムアウト処理が重要
- デバッグはサーバーログとネットワークログ
Streamable HTTP
- HTTPステータスコードでエラー判定
- リトライロジックの実装が容易
- デバッグはHTTPログで簡単
7. セキュリティ考慮事項
STDIO
- ✅ ネットワークに露出しない(最も安全)
- ✅ 認証不要
- ❌ プロセス権限管理が重要
SSE
- ⚠️ HTTPベースなので認証が必要
- ⚠️ CORS設定が必要な場合がある
- ⚠️ HTTPS推奨
- ⚠️ レート制限の実装推奨
Streamable HTTP
- ⚠️ HTTPベースなので認証が必要
- ⚠️ APIキーやトークンの管理
- ⚠️ HTTPS必須
- ⚠️ レート制限とDDoS対策が重要
どれを選ぶべきか
決定フローチャート
ローカルでのみ使用?
YES → STDIO を選択
NO ↓
リアルタイムのサーバープッシュが必要?
YES → SSE を選択
NO ↓
高いスケーラビリティが必要?
YES → Streamable HTTP を選択
NO ↓
実装のシンプルさを優先?
YES → STDIO または Streamable HTTP
NO → ユースケースに応じて選択
推奨シナリオ
初心者向け / 個人プロジェクト
推奨: STDIO
- 理由: 最もシンプルで学習コストが低い
- セットアップが簡単
- Claude Desktopですぐに試せる
中小規模のWebアプリケーション
推奨: SSE
- 理由: リアルタイム性とHTTPの利点を両立
- Webベースのクライアントと統合しやすい
- 適度な複雑さとパフォーマンス
エンタープライズ / 大規模システム
推奨: Streamable HTTP
- 理由: スケーラビリティと既存インフラとの統合
- ロードバランサーやKubernetesとの相性が良い
- マイクロサービスアーキテクチャに適合
ハイブリッドアプローチ
複数のトランスポートを同時にサポートすることも可能です:
// 同じMCPサーバーロジックを共有
const server = createMCPServer();
// STDIOで起動
if (process.env.TRANSPORT === 'stdio') {
const transport = new StdioServerTransport();
await server.connect(transport);
}
// HTTPで起動
if (process.env.TRANSPORT === 'http') {
const app = express();
// HTTPエンドポイントを設定
}
まとめ
各実装の特徴まとめ
項目 | STDIO | SSE | Streamable HTTP |
---|---|---|---|
実装難易度 | ⭐ 易 | ⭐⭐ 中 | ⭐⭐ 中 |
パフォーマンス | ⭐⭐⭐ 最高 | ⭐⭐ 良 | ⭐⭐ 良 |
スケーラビリティ | ⭐ 低 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
リアルタイム性 | ⭐⭐⭐ 最高 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
デバッグのしやすさ | ⭐⭐ 中 | ⭐⭐ 中 | ⭐⭐⭐ 易 |
本番環境適性 | ⭐ 低 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
重要なポイント
- STDIO: シンプルさとパフォーマンスを優先する場合の最適解
- SSE: リアルタイム性とネットワーク対応のバランスが良い
- Streamable HTTP: 汎用性とスケーラビリティで本番環境に最適
次のステップ
実際に3つの実装を試してみましょう:
# プロジェクトをクローン
git clone https://github.com/nogataka/mcp-server-samples.git
cd mcp-server-samples
# 一括セットアップ
chmod +x setup.sh
./setup.sh
# 各実装を試す
cd mcp-stdio && npm start
cd ../mcp-sse && npm start
cd ../mcp-streamable-http && npm start
それぞれの実装の違いを体感し、自分のユースケースに最適なものを選択してください。
参考リンク
- このプロジェクトのGitHubリポジトリ
- Model Context Protocol 公式ドキュメント
- MCP TypeScript SDK
- Server-Sent Events (MDN)
- JSON-RPC 2.0 仕様
ライセンス
MIT License
著者
この記事で使用されているコード例は、学習目的で自由に使用できます。
コントリビューション
改善提案やバグ報告は、GitHub Issuesでお願いします。
最終更新: 2025年1月