1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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月

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?