MCP Gateway — AIエージェントのツール呼び出しを誰がコントロールしているのか
MCPが月間9,700万ダウンロードを突破し事実上の標準になったが、エージェントがどのツールをどれだけ呼び出すかを制御するレイヤーは欠けている。MCP Gatewayパターンでこの課題を解決する。
私のClaude Codeセッション一つが、7つのMCPサーバーに接続されている。GitHub、Notion、Google Calendar、Gmail、Chrome DevTools、NotebookLM、そしてTelegram。このエージェントは私のメールを読み、カレンダーに予定を作り、Notionページを編集し、Chromeタブを開くことができる。
で、これを誰が監視しているのか?
誰もいない。少なくとも今の私のローカル環境では。
MCPは成功した。セキュリティレイヤーはまだだ
MCP(Model Context Protocol)の成長は凄まじい。Python + TypeScript SDKの合計月間ダウンロード数が9,700万を超え、Anthropic、OpenAI、Google、Microsoft、Amazonがすべてサポートしている。2024年末にAnthropicが作り、2025年12月にLinux FoundationのAAIFに寄付されて以来、事実上「AIエージェントが外部ツールを呼び出す方法」の標準となった。
問題は、このプロトコルが接続にフォーカスしていて、制御にはあまり関心がないということだ。
MCPサーバーを作るとツール(tool)を定義し、クライアントがそのツールを呼び出す。認証?OAuth 2.1がスペックに入った。しかし「このエージェントがこのツールを1日に何回まで呼び出せるか」「機密データを返すツールは承認なしで呼び出してはならない」といったポリシーレイヤーはMCPプロトコル自体にはない。それは実装側の責任だ。
そこで出てきた概念がMCP Gatewayだ。
MCP Gatewayとは何か
API Gatewayを想像すればいい。KongやAWS API Gatewayのようにバックエンドの前にプロキシを置くように、MCPサーバー群の前にプロキシを一つ置く。
エージェント → MCP Gateway → MCPサーバー群
Gatewayがやること:
- 認証/認可:どのエージェントがどのツールにアクセスできるか
- レートリミット:ツール呼び出し頻度の制限
- 監査ログ:誰がいつどのツールを呼んだか全て記録
- ポリシー適用:特定のツールは人間の承認後にのみ実行
- トラフィックルーティング:リクエストを適切なMCPサーバーに転送
私はこれをローカル環境で簡単にテストしてみた。Node.jsでMCPプロキシを一つ作り、Claude Codeと実際のMCPサーバーの間に挟む方式だ。
// 最もシンプルなMCP Gatewayの骨格
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const gateway = new Server({ name: "mcp-gateway", version: "0.1.0" }, {
capabilities: { tools: {} }
});
// ポリシーエンジン — ここで呼び出しを許可/拒否する
const policy = {
"gmail_read_message": { rateLimit: 10, requireApproval: false },
"gmail_create_draft": { rateLimit: 5, requireApproval: true },
"gcal_delete_event": { rateLimit: 2, requireApproval: true },
"notion-update-page": { rateLimit: 20, requireApproval: false },
};
const callCount: Record<string, number> = {};
gateway.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const rule = policy[toolName];
// レートリミットチェック
callCount[toolName] = (callCount[toolName] || 0) + 1;
if (rule && callCount[toolName] > rule.rateLimit) {
return {
content: [{ type: "text", text: `Rate limit exceeded for ${toolName}` }],
isError: true,
};
}
// 承認が必要なツールはブロック
if (rule?.requireApproval) {
console.error(`[GATEWAY] Approval required for: ${toolName}`);
// 実際にはここでSlack/Telegramに承認リクエストを送る
}
// 監査ログ
console.error(`[AUDIT] ${new Date().toISOString()} | ${toolName} | args: ${JSON.stringify(request.params.arguments)}`);
// 実際のMCPサーバーにフォワード(ここでは省略)
return await forwardToUpstream(toolName, request.params.arguments);
});
このコードが実際にプロダクションで使えるか?正直まだだ。しかしコアアイデアはこれだけで十分伝わる。エージェントのツール呼び出しは必ず一箇所を経由すべきで、その一箇所でポリシーを適用できなければならない。
実際に動かしてみたら足りないものが見えた
上のコードをClaude Codeに挟んで動かしてみた。結論から言うと——そのままでは動かない。
最初の問題はツールリストの同期だ。GatewayがCallToolRequestをインターセプトするには、まずクライアント(Claude Code)に「これらのツールがある」と伝える必要がある。上のコードにはlistToolsハンドラーがない。upstream MCPサーバーに接続してツールリストを取得し、それをそのままクライアントに伝えるパートを自分で作る必要がある。
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
gateway.setRequestHandler(ListToolsRequestSchema, async () => {
const upstreamTools = await fetchToolsFromUpstream();
return { tools: upstreamTools };
});
これだけで動くには動くが、upstreamサーバーが複数あるとツール名が衝突する可能性がある。私の環境ではGmailとGoogle Calendarが両方ともlistのようなgenericなツール名を公開していたので、ネームスペースを付ける必要があった。
2つ目の問題はレートリミットの寿命だ。上のコードのcallCountはメモリ上にある。プロセスを再起動するとカウントが0に戻る。Claude CodeはセッションごとにMCPサーバーを新しく起動するので、セッションが変わるたびにリミットがリセットされる。「1日10回」というポリシーをちゃんと守れないということだ。
3つ目に、requireApprovalをconsole.errorで出力するのは全く意味がなかった。stderrログをリアルタイムで見ている人がいないから。実際に承認を得るには外部チャネル(Telegram、Slack)にリクエストを送って応答が来るまでブロックする必要があるが、stdio基盤のMCPで非同期待機を実装するのはかなり面倒だ。
この3つを一度に解決する方法は何かと考えたが、少なくとも1つ目と2つ目の答えはシンプルだ。監査ログをファイルやメモリではなくSQLiteに書けばいい。
監査ログをSQLiteへ
console.errorで流していた監査ログをSQLiteテーブルに保存すれば、レートリミットの寿命問題も同時に解決する。プロセスが再起動してもDBは残るから。
import Database from "better-sqlite3";
const db = new Database("mcp-audit.db");
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT (datetime('now')),
tool_name TEXT NOT NULL,
args TEXT,
result_status TEXT DEFAULT 'ok',
latency_ms INTEGER,
blocked INTEGER DEFAULT 0,
block_reason TEXT
)
`);
const insertLog = db.prepare(`
INSERT INTO audit_log (tool_name, args, result_status, latency_ms, blocked, block_reason)
VALUES (?, ?, ?, ?, ?, ?)
`);
const countToday = db.prepare(`
SELECT COUNT(*) as cnt FROM audit_log
WHERE tool_name = ? AND timestamp > datetime('now', '-1 day') AND blocked = 0
`);
既存のcallCountディクショナリの代わりにcountTodayクエリを使えば、セッションが変わっても「今日Gmailの読み取りを何回呼んだか」を正確に追跡できる。Gatewayのハンドラーはこう変わる:
gateway.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const rule = policy[toolName];
const start = Date.now();
if (rule) {
const { cnt } = countToday.get(toolName) as { cnt: number };
if (cnt >= rule.rateLimit) {
insertLog.run(toolName,
JSON.stringify(request.params.arguments),
"blocked", 0, 1, "rate_limit");
return {
content: [{ type: "text",
text: `Rate limit exceeded: ${toolName} (${cnt}/${rule.rateLimit} today)` }],
isError: true,
};
}
}
const result = await forwardToUpstream(
toolName, request.params.arguments);
const latency = Date.now() - start;
insertLog.run(toolName,
JSON.stringify(request.params.arguments),
"ok", latency, 0, null);
return result;
});
溜まったログで何ができるか
数日動かすとmcp-audit.dbにデータが溜まった。これで出来ることが思った以上に多い。
ツール別呼び出し頻度——どのツールが最も多く呼ばれているか一目で分かる。
SELECT tool_name, COUNT(*) as calls, ROUND(AVG(latency_ms)) as avg_ms
FROM audit_log WHERE blocked = 0
GROUP BY tool_name ORDER BY calls DESC LIMIT 10;
私の場合notion-searchが圧倒的1位だった。エージェントが何かをする前にまずNotionを検索するパターンがあるようだ。これを見てNotion検索結果のキャッシングが意味あるかもと思った。
ブロック率——レートリミットに引っかかった呼び出しが全体の何パーセントか。
SELECT tool_name,
SUM(CASE WHEN blocked = 1 THEN 1 ELSE 0 END) as blocked,
COUNT(*) as total,
ROUND(100.0 * SUM(blocked) / COUNT(*), 1) as block_rate
FROM audit_log GROUP BY tool_name HAVING blocked > 0;
ブロック率が高ければ2つのうちどちらかだ。リミットが厳しすぎるか、エージェントが同じツールを繰り返し呼ぶ非効率なパターンを持っているか。後者ならプロンプトを見直すべきだ。
時間帯別パターン——エージェントが最も活発な時間帯はいつか。
SELECT strftime('%H', timestamp) as hour, COUNT(*) as calls
FROM audit_log GROUP BY hour ORDER BY hour;
当然の結果ではあるが、私のcronジョブが走る11時〜12時に呼び出しが集中する。チーム環境ならこのデータでMCPサーバーの負荷分散タイミングを決められるだろう。
このデータの本当の価値はポリシーチューニングの根拠になるということだ。「Gmailの読み取りは1日10回で足りるか?」という問いに勘で答えるのではなく、実際の利用パターンを見て判断できる。私の場合gmail_read_messageは1日平均3回しか使っておらず、リミット10は余裕だった。一方notion-searchは1日40回近く呼ばれていてリミット20では足りず、30に上げた。
実際に必要になる瞬間
「うちのチームはまだMCPをそんなに使ってないですけど」——この言い訳が通じる時代が終わりつつある。
私が実際に経験したケースを一つ挙げると、Claude CodeでNotion MCPを使ってページを編集中に、意図せず別チームのページに触れてしまったことがある。エージェントが検索結果から似たタイトルのページを選び、私は承認ボタンを何も考えずに押した。データが消えたわけではないが、気まずかった。
これが1人の開発者のローカルで起きれば気まずい程度だ。しかし50人のチームがエージェントを使い、各エージェントが5〜10個のMCPサーバーに接続されていたら?監査ログもなしに?誰がどのツールを呼んだか追跡もできないなら?
エンタープライズでMCP Gatewayが必要な本当の理由はセキュリティよりも可視性だ。エージェントが何をしているのか見えなければならない。
すでに登場しているソリューション
MCP Gatewayという名前で登場しているオープンソースと商用プロジェクトがすでにある。調べた限りでは大きく2つのアプローチがある。
1. プロキシ方式 — エージェントとMCPサーバーの間にリバースプロキシを置く。既存のAPI Gatewayとアーキテクチャが同じだ。設定がシンプルで既存インフラを再利用できるのがメリット。
2. サイドカー方式 — 各MCPサーバーにポリシーエンジンを付ける。サービスメッシュ(Istio、Linkerd)のサイドカーパターンと同一だ。より細かい制御が可能だが運用の複雑さが上がる。
小規模チームならプロキシ方式で十分だと私は思う。サイドカーまで行くのはMCPサーバーが20個以上あり、チームごとに異なるポリシーが必要な場合だが、その規模なら既に専任のプラットフォームエンジニアがいるはずだ。
ただし、これは過渡期の解法だ
ここで批判的に考えるべきことがある。
MCP Gatewayが必要だということは、MCPプロトコル自体にガバナンスレイヤーが欠けているということだ。HTTPの上にAPI Gatewayを載せるのはHTTPに認証がないからではなく、ビジネスロジックとトラフィック管理が必要だからだ。MCPも同様に、プロトコルレベルでポリシーを定義できる拡張が出る可能性が高い。
その時、今作ったGatewayがレガシーになる。
個人的には6ヶ月以内にMCPスペックにpolicy extensionのようなものが追加されると見ている。Linux Foundationに寄付された後、ガバナンス関連の議論が活発なのを見ると、方向性はすでに固まっているようだ。しかしその6ヶ月間ノーコントロールでエージェントを動かすのはリスキーなので、Gatewayはその間を埋めるブリッジソリューションだ。
もう一つ——Gatewayを導入するとエージェントのレスポンス速度が遅くなる。プロキシを一段経由するので当然だ。ローカルでテストしたところ、ツール呼び出し1回あたり50〜100msのオーバーヘッドが追加された。ほとんどの場合は体感できないが、LLMが1タスクでツールを20〜30回呼ぶパターンでは全体で1〜2秒追加され、ユーザー体験に影響しうる。
まだ解決していないこと
SQLiteでログを溜めてポリシーをチューニングするところまでは一人でもできる。しかしrequireApproval——人間の承認を得るパートはまだちゃんと実装できていない。
次に試すのはTelegramボット連携だ。requireApproval: trueのツール呼び出しが来たらTelegramで承認リクエストを送り、ユーザーが「OK」を押すまでGatewayがリクエストをホールドする方式。アイデアはシンプルだが、stdio基盤のMCPでこれを非同期処理するには構造を変える必要がある。今はリクエストが来たらすぐレスポンスを返す同期構造なので。
そして根本的に、これは個人開発者のローカル環境でしか意味のないレベルだ。チーム単位で使うにはGateway自体の認証、マルチテナンシー、ポリシー管理UIなどが必要で、そこまで来ると自作ではなくプロダクトを使うのが正解だ。
AIエージェントにツールを渡す時、「何ができるか」と同じくらい「何をさせないか」が重要だ。MCP Gatewayは後者のための最も現実的な出発点であり、SQLite一つ付けるだけで「自分のエージェントが何をしているか」が見え始める。そこからポリシーはデータで決められる。
他の言語で読む
- 🇰🇷 한국어
- 🇯🇵 日本語(現在のページ)
- 🇺🇸 English
- 🇨🇳 中文
この記事は役に立ちましたか?
より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください。