📑

MCP Server 実装完全ガイド: STDIO vs SSE vs Streamable HTTP

に公開

GitHub

はじめに

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つの異なるトランスポート方式で実装することで、それぞれの特徴と使い分けを理解できます。

目次

  1. MCPの基本概念
  2. 3つのトランスポート方式の概要
  3. STDIO実装の詳細
  4. SSE実装の詳細
  5. Streamable HTTP実装の詳細
  6. 徹底比較
  7. どれを選ぶべきか

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
実装難易度 ⭐ 易 ⭐⭐ 中 ⭐⭐ 中
パフォーマンス ⭐⭐⭐ 最高 ⭐⭐ 良 ⭐⭐ 良
スケーラビリティ ⭐ 低 ⭐⭐ 中 ⭐⭐⭐ 高
リアルタイム性 ⭐⭐⭐ 最高 ⭐⭐⭐ 高 ⭐⭐ 中
デバッグのしやすさ ⭐⭐ 中 ⭐⭐ 中 ⭐⭐⭐ 易
本番環境適性 ⭐ 低 ⭐⭐ 中 ⭐⭐⭐ 高

重要なポイント

  1. STDIO: シンプルさとパフォーマンスを優先する場合の最適解
  2. SSE: リアルタイム性とネットワーク対応のバランスが良い
  3. 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

それぞれの実装の違いを体感し、自分のユースケースに最適なものを選択してください。


参考リンク


ライセンス

MIT License

著者

@nogataka

この記事で使用されているコード例は、学習目的で自由に使用できます。

コントリビューション

改善提案やバグ報告は、GitHub Issuesでお願いします。


最終更新: 2025年1月

Discussion