複数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_COMMAND
とMCP_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スクリプトの実行が環境によって不安定
原因:
- WSL起動のオーバーヘッド
- bashスクリプトのプロセス起動が環境によって遅延
- 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プロキシ方式の主な利点
- ⚡ 高速: 常駐コンテナで0.3-0.5秒のレスポンス(従来比6-10倍高速化)
- 🗑️ クリーン: ゴミコンテナが溜まらない
- 🔒 セキュア: ツールフィルタリングで破壊的操作を一括ブロック
- 👥 チーム運用: フィルタリング設定をプロキシ側で統一管理
- 🎯 統合管理: 複数プロジェクト/複数MCPサーバーを1つのプロキシで管理
- 📝 シンプル: 設定ファイル(portal-config.json)で一元管理
- 🔧 拡張性: 共通STDIO Adapterで新しいMCPサーバーを簡単に追加
- 🏠 ローカル環境: Docker Composeで簡単にローカル構築
- 🚀 統一クライアント: Node.js HTTPクライアント1つでCursor/Claude Desktop両方をサポート
- 💪 安定性: 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秒へ大幅改善
実装の変遷:
-
初期: bashスクリプト(
mcp-portal-proxy.sh
)方式 → Cursorで不安定 -
改善: Node.js HTTPクライアント(
mcp-portal-client.js
)追加 → Claude Desktopで安定動作 - 統一: CursorでもNode.js方式に統一 → 全環境で安定・高速動作を実現
チームでMCPサーバーを使う際の課題を抱えている方の参考になれば幸いです。
Discussion