💻

複数MCPサーバーを統合管理する軽量HTTPプロキシシステムの構築

に公開

複数MCPサーバーを統合管理する軽量HTTPプロキシシステムの構築

はじめに

前提: このシステムはローカル開発環境(localhost)での使用を想定しています。

Backlog MCPサーバーを複数プロジェクトやワークスペースで使いたいとき、従来は以下のような設定でした:

{
  "mcpServers": {
    "backlog": {
      "command": "wsl",
      "args": ["-e", "bash", "-l", "-c",
        "BACKLOG_DOMAIN='example.backlog.com' BACKLOG_API_KEY='xxx' docker run --pull always -i --rm -e BACKLOG_DOMAIN -e BACKLOG_API_KEY ghcr.io/nulab/backlog-mcp-server"
      ]
    }
  }
}

この方式の問題点

  • 🗑️ ゴミコンテナが溜まる: リクエストごとにdocker run -i --rmが実行され、異常終了時にコンテナが残る
  • 🔓 ツールフィルタリングが面倒: 破壊的操作(delete_project等)をブロックするには、各メンバーがツールごとに設定する必要がある
  • 👥 チーム管理が大変: 各メンバーが個別に設定ファイルを管理し、同じフィルタリング設定を繰り返し記述
  • ⚠️ 環境変数の管理が煩雑: APIキーがClaude/Cursorの設定ファイルに直接記載される

本記事の目的
これらの問題を解決する軽量HTTPプロキシシステムを構築し、チーム全体で共通のフィルタリング設定を適用できるようにします。


システムアーキテクチャ

複数のMCPサーバーを統合管理するHTTPプロキシシステムです。

ローカル環境での運用:

  • Docker Composeでローカルに構築
  • localhost:18080でプロキシサーバーを起動
  • チームメンバーは同じプロキシを参照することで、統一されたツールフィルタリングを適用

全体構成

┌─────────────────────────────────────────────────┐
│ Claude Desktop / Cursor (ローカル環境)           │
│  - STDIO経由でMCPプロトコル通信                    │
└─────────────────┬───────────────────────────────┘
                  │ (STDIO → HTTP変換)
                  │ mcp-portal-client.js (Node.js HTTPクライアント)
                  ↓
┌─────────────────────────────────────────────────┐
│ HTTPプロキシサーバー (Starlette/Python)           │
│  localhost:18080                                │
│  - ツールフィルタリング (blacklist/whitelist)      │
│    → チーム共通の設定を一元管理                    │
│  - 複数サーバーのルーティング                       │
│  - 環境変数の一元管理                              │
└─────────────────┬───────────────────────────────┘
                  │ (HTTP)
        ┌─────────┴─────────┬──────────────┐
        │                   │              │
        ↓                   ↓              ↓
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│ Backlog MCP   │   │ GitHub MCP    │   │ Repomix MCP   │
│ (STDIO Adapter)│   │ (STDIO Adapter)│   │ (STDIO Adapter)│
│ :8001         │   │ :8003         │   │ :8005         │
└───────────────┘   └───────────────┘   └───────────────┘

メリット比較表

項目 従来方式 (docker run) HTTPプロキシ方式
コンテナ管理 ゴミコンテナが溜まる 常駐コンテナで管理が容易
起動時間 3-5秒/リクエスト 0.3-0.5秒/リクエスト
ツールフィルタリング 各メンバーが個別設定 設定ファイルで一括管理
チーム運用 メンバーごとに設定が必要 プロキシ参照のみで統一
複数プロジェクト管理 個別にdocker run 1つのプロキシで統合
環境変数管理 設定ファイルに直接記載 .envで一元管理
ポート管理 コンテナごとに公開 プロキシのみ公開
セキュリティ 全ツール利用可能 破壊的操作をフィルタリング可能
クライアント安定性 環境依存で不安定 Node.js統一で安定動作

実装: ステップバイステップ

1. プロキシサーバー本体 (Python/Starlette)

軽量ASGIフレームワーク「Starlette」を使用したHTTPプロキシサーバーです。

主な機能

  • HTTPエンドポイント: POST /servers/{server_name}/mcp/
  • ツールフィルタリング: config/portal-config.json
  • 複数サーバー管理: Docker Composeで各MCPアダプターと連携

2. STDIO to HTTPアダプター(共通化)

課題: Backlog, GitHub, Repomix等のMCPサーバーはSTDIO専用。HTTP化が必要。

解決策: 全STDIO MCPサーバーで共通利用可能なNode.js Expressアダプターを実装

// mcp-stdio-adapter.js
const express = require('express');
const { spawn } = require('child_process');

const MCP_COMMAND = process.env.MCP_COMMAND || 'npx';
const MCP_ARGS = process.env.MCP_ARGS ? process.env.MCP_ARGS.split(' ') : [];

app.post('/', async (req, res) => {
  const jsonrpcRequest = req.body;

  // JSON-RPC通知(idがない)の検出
  const isNotification = !jsonrpcRequest.hasOwnProperty('id');

  // MCPサーバープロセスを起動
  const mcp = spawn(MCP_COMMAND, MCP_ARGS, {
    env: process.env,
    stdio: ['pipe', 'pipe', 'pipe']
  });

  let stdout = '';

  mcp.stdout.on('data', (data) => {
    stdout += data.toString();
  });

  mcp.on('close', (code) => {
    // 通知の場合は204 No Contentを返す
    if (isNotification) {
      return res.status(204).send();
    }

    // 通常のレスポンス処理
    const response = JSON.parse(stdout);
    res.json(response);
  });

  // リクエストを送信(通知でも送信)
  mcp.stdin.write(JSON.stringify(jsonrpcRequest) + '\n');
  mcp.stdin.end();
});

ポイント:

  • 汎用性: MCP_COMMANDMCP_ARGS環境変数で任意のSTDIO MCPサーバーに対応
  • 通知対応: JSON-RPC通知(notifications/initialized等)を正しく処理し204 No Contentを返却
  • ステートレス: リクエストごとにプロセスを起動するシンプル設計

3. Node.js HTTPクライアント(統一クライアント)

課題:

  • Bashスクリプト方式は環境によって動作が不安定
  • Cursorで「no tools」エラーが発生
  • Claude Desktopでタイムアウト(5秒制限)

解決策: Node.js HTTPクライアントで統一し、安定性とパフォーマンスを両立

// mcp-portal-client.js
#!/usr/bin/env node
const http = require('http');
const readline = require('readline');

const PORTAL_URL = process.env.PORTAL_URL || 'http://localhost:18080/servers/backlog/mcp/';
const url = new URL(PORTAL_URL);

const rl = readline.createInterface({
  input: process.stdin,
  terminal: false
});

rl.on('line', (line) => {
  const requestData = JSON.parse(line);

  if (!requestData.hasOwnProperty('id')) {
    return; // 通知は無視
  }

  const options = {
    hostname: url.hostname,
    port: url.port || 80,
    path: url.pathname,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(line)
    },
    timeout: 4000 // 4秒タイムアウト
  };

  const req = http.request(options, (res) => {
    let data = '';
    res.on('data', (chunk) => { data += chunk; });
    res.on('end', () => {
      if (res.statusCode === 200) {
        console.log(data);
      }
    });
  });

  req.write(line);
  req.end();
});

メリット:

  • 高速: 0.3-0.5秒のレスポンス時間
  • 安定: 環境依存の問題を解消
  • シンプル: 標準ライブラリのみで実装
  • 統一: Cursor/Claude Desktop両方で同じクライアント使用

4. Docker Compose構成

services:
  mcp-portal:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "127.0.0.1:${MCP_PORTAL_PORT:-18080}:8000"
    volumes:
      - ./config:/app/config:ro
      - ./logs:/app/logs
    env_file: .env
    restart: always
    networks:
      - mcp-network

  # 共通STDIO Adapterを使用したBacklog MCP
  backlog-mcp:
    build:
      context: .
      dockerfile: Dockerfile.stdio-adapter
      args:
        MCP_INSTALL_PACKAGE: "backlog-mcp-server"
    environment:
      - ADAPTER_PORT=8001
      - MCP_COMMAND=npx
      - MCP_ARGS=backlog-mcp-server --prefix backlog_
      - BACKLOG_DOMAIN=${BACKLOG_DOMAIN}
      - BACKLOG_API_KEY=${BACKLOG_API_KEY}
    restart: always
    networks:
      - mcp-network

  # 共通STDIO Adapterを使用したGitHub MCP
  github-mcp:
    build:
      context: .
      dockerfile: Dockerfile.stdio-adapter
      args:
        MCP_INSTALL_PACKAGE: "@modelcontextprotocol/server-github"
    environment:
      - ADAPTER_PORT=8003
      - MCP_COMMAND=npx
      - MCP_ARGS=@modelcontextprotocol/server-github
      - GITHUB_TOKEN=${GITHUB_TOKEN}
    restart: always
    networks:
      - mcp-network

  # 共通STDIO Adapterを使用したRepomix MCP
  repomix-mcp:
    build:
      context: .
      dockerfile: Dockerfile.stdio-adapter
      args:
        MCP_INSTALL_PACKAGE: "repomix"
    environment:
      - ADAPTER_PORT=8005
      - MCP_COMMAND=npx
      - MCP_ARGS=-y repomix --mcp
    restart: always
    networks:
      - mcp-network

networks:
  mcp-network:

Dockerfile.stdio-adapter(共通化):

FROM node:20-alpine

WORKDIR /adapter

# 環境変数でMCPサーバーを指定可能
ARG MCP_INSTALL_PACKAGE
RUN if [ -n "$MCP_INSTALL_PACKAGE" ]; then npm install -g $MCP_INSTALL_PACKAGE; fi

# Expressをインストール
RUN npm install express

# 共通アダプタースクリプトをコピー
COPY mcp-stdio-adapter.js /adapter/adapter.js

CMD ["node", "/adapter/adapter.js"]

メリット:

  • 共通化: 1つのDockerfileで全STDIO MCPサーバーに対応
  • 柔軟性: MCP_INSTALL_PACKAGEビルド引数でパッケージ指定
  • 設定の分離: 環境変数は.envファイルで一元管理
  • 高速: 常駐コンテナで高速レスポンス

5. ツールフィルタリング設定

// config/portal-config.json
{
  "version": "1.0",
  "servers": [
    {
      "name": "backlog",
      "url": "http://backlog-mcp:8001",
      "environment_variables": ["BACKLOG_API_KEY"],
      "timeout": 30.0,
      "enabled": true,
      "tool_filter": {
        "mode": "blacklist",
        "blocked_tools": [
          "backlog_add_project",
          "backlog_delete_project",
          "backlog_update_project",
          "backlog_delete_issue",
          "backlog_delete_wiki"
        ]
      }
    },
    {
      "name": "github",
      "url": "http://github-mcp:8003",
      "environment_variables": ["GITHUB_TOKEN"],
      "timeout": 30.0,
      "enabled": true,
      "tool_filter": {
        "mode": "blacklist",
        "blocked_tools": [
          "create_or_update_file",
          "search_repositories",
          "create_repository",
          "push_files",
          "create_issue",
          "fork_repository"
        ]
      }
    },
    {
      "name": "repomix",
      "url": "http://repomix-mcp:8005",
      "environment_variables": [],
      "timeout": 30.0,
      "enabled": true,
      "description": "Code repository packaging and analysis"
    }
  ]
}

セキュリティ強化: 破壊的操作を簡単にブロック


リクエスト処理フロー

全体の流れ

1. Claude Desktop/Cursor
   ↓ (STDIO) {"jsonrpc":"2.0","id":1,"method":"tools/list"}

2. mcp-portal-client.js (Node.js HTTPクライアント)
   ↓ STDIO → HTTP変換
   ↓ POST http://localhost:18080/servers/backlog/mcp/

3. HTTPプロキシサーバー (Starlette)
   ├─ portal-config.json を読み込み
   ├─ "backlog" の設定を取得
   │  - url: http://backlog-mcp:8001
   │  - tool_filter: blocked_tools = [...]
   ├─ HTTPリクエストを送信
   ↓ POST http://backlog-mcp:8001/

4. mcp-stdio-adapter.js (共通アダプター)
   ├─ HTTPリクエストを受信
   ├─ Backlog MCPサーバープロセスを起動
   ├─ JSON-RPCリクエストをSTDINに送信
   ├─ STDOUTからレスポンス受信
   └─ HTTPレスポンスとして返却
   ↓ HTTP 200 OK + JSON-RPC response

5. HTTPプロキシサーバー
   ├─ レスポンス受信
   ├─ ツールフィルタリング適用
   │  - blocked_toolsを除外
   └─ フィルタリング済みレスポンス返却
   ↓ HTTP 200 OK + フィルタリング済みJSON-RPC response

6. mcp-portal-client.js
   ↓ HTTP → STDIO変換
   ↓ レスポンスをSTDOUTに出力

7. Claude Desktop/Cursor
   ← (STDIO) フィルタリング済みツール一覧

処理時間の内訳:

  • クライアント → プロキシ: 0.05-0.1秒(ローカルHTTP通信)
  • プロキシ → MCPアダプター: 0.1-0.2秒(Docker内部通信)
  • MCPアダプター処理: 0.2-0.3秒(STDIO変換 + MCPサーバー実行)
  • 合計: 0.3-0.5秒(従来方式の3-5秒から大幅改善)

躓きポイントと解決策

1. JSON-RPC通知の処理エラー

問題: notifications/initializedで500エラー

Failed to parse response: SyntaxError: Unexpected end of JSON input

原因: JSON-RPC通知(idがない)にはレスポンスを返してはいけないが、アダプターが応答を期待してパースしようとした。

解決:

// 通知判定
const isNotification = !jsonrpcRequest.hasOwnProperty('id');

mcp.on('close', (code) => {
  // 通知の場合はプロセス完了後に204 No Contentを返す
  if (isNotification) {
    return res.status(204).send();
  }
  // 通常処理
});

// 通知でもリクエストをstdinに送信
mcp.stdin.write(JSON.stringify(jsonrpcRequest) + '\n');

2. Content-Lengthヘッダー不一致エラー

問題: レスポンスが大きい(34KB)とクライアント側でエラー

Response content longer than Content-Length

原因: StarletteのStreamingResponseが元のHTTPレスポンスのContent-Lengthヘッダーをそのまま転送していた

解決: プロキシサーバー側でContent-Lengthヘッダーを削除

# src/proxy/streaming.py
# Content-Lengthヘッダーを削除(Responseが自動的に設定する)
response_headers = dict(response.headers)
response_headers.pop('Content-Length', None)
response_headers.pop('content-length', None)

return Response(
    content=response.content,
    status_code=response.status_code,
    headers=response_headers
)

3. クライアント接続の安定性問題

問題:

  • Claude Desktop(Windows + WSL)でタイムアウトエラー(5秒制限)
  • Cursorでツールが読み込めない("no tools")
  • bashスクリプトの実行が環境によって不安定

原因:

  1. WSL起動のオーバーヘッド
  2. bashスクリプトのプロセス起動が環境によって遅延
  3. curlのバッファリング問題

パフォーマンス実測:

  • Windows → Portal直接: 0.88秒
  • WSL bashスクリプト経由(旧方式): 不安定 → ツールが読み込めないケースあり ❌
  • WSL Node.jsクライアント(新方式): 0.3-0.5秒 → Cursor/Claude Desktop両方で安定動作 ✅

解決策: Node.js HTTPクライアント(mcp-portal-client.js)に統一


Claude Desktop / Cursor設定

統一設定方式(推奨)

Linux/macOS(Cursor/Claude Desktop共通)

~/.cursor/mcp.json または ~/.config/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "backlog": {
      "command": "/path/to/node",
      "args": ["/home/username/mcp-portal/mcp-portal-client.js"],
      "env": {
        "PORTAL_URL": "http://localhost:18080/servers/backlog/mcp/"
      }
    },
    "github": {
      "command": "/path/to/node",
      "args": ["/home/username/mcp-portal/mcp-portal-client.js"],
      "env": {
        "PORTAL_URL": "http://localhost:18080/servers/github/mcp/"
      }
    },
    "repomix": {
      "command": "/path/to/node",
      "args": ["/home/username/mcp-portal/mcp-portal-client.js"],
      "env": {
        "PORTAL_URL": "http://localhost:18080/servers/repomix/mcp/"
      }
    }
  }
}

Node.jsパスの確認:

which node
# 出力例: /home/username/.nvm/versions/node/v20.19.5/bin/node

Windows + WSL(Cursor/Claude Desktop共通)

%APPDATA%\Claude\claude_desktop_config.json または Cursorの設定ファイル:

{
  "mcpServers": {
    "backlog": {
      "command": "wsl",
      "args": [
        "-e",
        "bash",
        "-c",
        "PORTAL_URL='http://localhost:18080/servers/backlog/mcp/' /home/username/.nvm/versions/node/v20.19.5/bin/node /home/username/mcp-portal/mcp-portal-client.js"
      ],
      "env": {}
    },
    "github": {
      "command": "wsl",
      "args": [
        "-e",
        "bash",
        "-c",
        "PORTAL_URL='http://localhost:18080/servers/github/mcp/' /home/username/.nvm/versions/node/v20.19.5/bin/node /home/username/mcp-portal/mcp-portal-client.js"
      ],
      "env": {}
    },
    "repomix": {
      "command": "wsl",
      "args": [
        "-e",
        "bash",
        "-c",
        "PORTAL_URL='http://localhost:18080/servers/repomix/mcp/' /home/username/.nvm/versions/node/v20.19.5/bin/node /home/username/mcp-portal/mcp-portal-client.js"
      ],
      "env": {}
    }
  }
}

WSL内のNode.jsパス確認:

wsl -e which node
# 出力例: /home/username/.nvm/versions/node/v20.19.5/bin/node

統一設定のメリット:

  • 安定性: bashスクリプトの不安定さを解消し、Cursor/Claude Desktop両方で確実に動作
  • パフォーマンス: Node.js HTTPクライアントで高速化(0.3-0.5秒)
  • シンプル: 1つのクライアント実装で全環境に対応

チーム運用:

  • 各メンバーは同じプロキシURL(http://localhost:18080)を参照
  • フィルタリング設定はプロキシ側で一元管理
  • プロキシサーバーを起動している人の設定が全員に適用される

まとめ

HTTPプロキシ方式の主な利点

  1. ⚡ 高速: 常駐コンテナで0.3-0.5秒のレスポンス(従来比6-10倍高速化)
  2. 🗑️ クリーン: ゴミコンテナが溜まらない
  3. 🔒 セキュア: ツールフィルタリングで破壊的操作を一括ブロック
  4. 👥 チーム運用: フィルタリング設定をプロキシ側で統一管理
  5. 🎯 統合管理: 複数プロジェクト/複数MCPサーバーを1つのプロキシで管理
  6. 📝 シンプル: 設定ファイル(portal-config.json)で一元管理
  7. 🔧 拡張性: 共通STDIO Adapterで新しいMCPサーバーを簡単に追加
  8. 🏠 ローカル環境: Docker Composeで簡単にローカル構築
  9. 🚀 統一クライアント: Node.js HTTPクライアント1つでCursor/Claude Desktop両方をサポート
  10. 💪 安定性: bashスクリプトの不安定さを解消し、確実な動作を実現

今後の改善案

  • 認証・認可: ユーザーごとにアクセス制御
  • 監査ログ: 全リクエストのロギング
  • レート制限: APIクォータ管理
  • キャッシング: 頻繁に使われるツールのレスポンスをキャッシュ
  • Web UI: ツール使用状況のダッシュボード

おわりに

HTTPプロキシ方式を導入することで、ローカル環境で複数のMCPサーバーを効率的かつセキュアに管理できるようになりました。

特に以下のメリットは実用上非常に大きいです:

  • ゴミコンテナが溜まらない: 開発環境がクリーンに保たれる
  • チーム全体で統一されたフィルタリング: 各メンバーが個別に設定する必要がない
  • 共通STDIO Adapter: 1つのDockerfileで全STDIO MCPサーバーに対応
  • 統一クライアント: Node.js HTTPクライアント1つでCursor/Claude Desktop両方に対応
  • 安定性: bashスクリプト方式の不安定さを解消し、確実な動作を実現
  • 高速化: 従来方式(3-5秒)から0.3-0.5秒へ大幅改善

実装の変遷:

  1. 初期: bashスクリプト(mcp-portal-proxy.sh)方式 → Cursorで不安定
  2. 改善: Node.js HTTPクライアント(mcp-portal-client.js)追加 → Claude Desktopで安定動作
  3. 統一: CursorでもNode.js方式に統一 → 全環境で安定・高速動作を実現

チームでMCPサーバーを使う際の課題を抱えている方の参考になれば幸いです。

Discussion