1. なぜもう1つのレイヤーが必要なのか?
多くの人はこう始めます。1 つの MCP サーバーがあり、いくつかのツールを公開し、ChatGPT が HTTPS でそれに直接アクセスする——一見完璧です。想定アーキテクチャは次のとおりです。
ChatGPT → あなたの MCP サーバー → データベース / 外部 API
「ペットプロジェクト」の段階では確かに悪くありません。しかし、アプリが機能を増やし、開発チームが拡大し始めると、問題がすぐに浮かび上がってきます。
第一に、MCP サーバーが「God object」化します。ギフト選定ツールも、チェックアウトも、アナリティクスも、ついでに「ここにレポーティングも突っ込もう」といったものも同居するようになります。コードの各部は異なる SLA やセキュリティ要件を持つのに、1 つのプロセスに貼り合わされています。
第二に、ChatGPT や他のクライアントが、あなたのサービスのトポロジを知っておく必要が出てきます。半年後に commerce 用の MCP サーバーがもう 1 つ増えたら、クライアントの再接続、設定や記述の変更が必要になります。「単一の入口」の代わりに、URL が乱立する状態になります。
第三に、全サービス共通の機能——認証、ログ、メトリクス、レート制限、トークン検証、ローカライゼーション、地域別ルーティング——をどこで実装すべきかが不明瞭になります。これをすべての MCP/Agent サービスにばらまけば、重複が増え、サービスごとに挙動もまちまちになります。
この結合を断ち切り、同時に ChatGPT から内部の複雑さを隠すために登場するのが MCP Gateway——MCP トラフィック全体のネットワークゲートウェイかつ単一の入口です。
2. ChatGPT App の文脈での MCP Gateway とは
形式的には MCP Gateway は、MCP クライアント(ChatGPT、MCP Jam、社内ツールなど)と複数のバックエンドサービス群(通常は REST/HTTP API、マイクロサービス、Agents サービス、commerce バックエンド等)の間にあるプロキシ層であり、単一の入口です。
Gateway 自身が外部に対して MCP プロトコルを実装します(ChatGPT からは 1 台の MCP サーバーに見える)。内部では通常の REST エンドポイントに HTTP/gRPC でアクセスするだけです。
tools/list へのリクエストに対して gateway は呼び出しをそのまま奥へはプロキシしません。代わりに自前のツール一覧を返します。これはコードにハードコードされているか、設定から集約されます。各ツールは特定の REST エンドポイントとデータスキーマに紐づきます。tools/call へのリクエストでは、gateway がツール名から対応する REST ルートを見つけ、fetch/HTTP クライアントで呼び出します。
概念図:
flowchart LR
ChatGPT["ChatGPT / モデル"] --> |MCP JSON-RPC| Gateway["MCP Gateway<br/>(単一の MCP サーバー)"]
Gateway --> GiftAPI["Gift REST API<br/>/ ギフト用マイクロサービス"]
Gateway --> CommerceAPI["Commerce REST API<br/>/ ACP / 決済"]
Gateway --> AnalyticsAPI["Analytics Service<br/>/ イベントとメトリクス"]
ChatGPT にとっては 1 台のサーバー——1 つの URL、1 つのツールセット、1 本のイベントストリーム。あなたにとっては、さまざまなコールド/ホットなサービスへの柔軟なトラフィック・ルーティングポイントです。
3. GiftGenius のアーキテクチャにおける MCP Gateway
抽象論を減らし「生きたシステム」で gateway を示すために、ギフトを提案し ACP/Instant Checkout で注文を処理できるアプリ GiftGenius の例を続けます。
シンプル版では 1 つの MCP サーバーが suggest_gifts と checkout_start の両方を持っていました。アプリが大きくなった今、責務を分離します。
- Gift REST API — ギフトの検索・推薦、カタログとフィードの管理(一般的な HTTP/REST サービス)。
- Commerce REST API — ACP、チェックアウトセッション、注文ステータス、決済プロバイダーとの連携。
- Analytics Service / REST API — イベントとメトリクスの収集(どの提案が開かれ、何が購入されるか)。
- 必要であれば別の Agents サービス — 複雑な多段シナリオ。これも MCP ではなく HTTP/REST 経由で利用します。
MCP Gateway はこれらすべてのコンポーネントへの単一の入口になります。Gateway は次を行います。
- リクエスト tools/list に対し、自身が定義した単一のツール一覧を返す。各ツールは特定のサービスの REST エンドポイントに結びつけられます。
- リクエスト tools/call に対し、ツール名(params.name)を見てルーティングテーブルから向かうべき REST サービスを決定し、対応する HTTP メソッドを呼び出します(fetch、axios など)。
suggest_gifts という tools/call が来たら、gateway は Gift REST API の対応する REST エンドポイントを呼び出します。checkout_start であれば、Commerce REST API へリクエストが送られます。
Express 風の TypeScript での簡単な擬似コードは次のようになります。
// MCP リクエストの超簡易ハンドラ
app.post("/mcp", async (req, res) => {
const mcpReq = req.body as { method: string; params?: any };
const ctx = buildContextFromHeaders(req); // 認証、ロケールなど
const toolName = mcpReq.params?.name;
const backendRes = await callBackend(toolName, mcpReq, ctx);
res.json(backendRes);
});
pickBackend の内部では、メソッド名やツール名、ユーザーのロケール、さらにはサービスのバージョン(このモジュールの後半で扱うカナリアや blue/green リリース)にも基づけます。
4. MCP Gateway の責務: 何を確実に行うか
GiftGenius のアーキテクチャにおける gateway を見ました。ここでは特定アプリに依存しない、独立した層としての責務を明確にしておきます。Gateway はネットワークかつクロスサービスの層として捉えることが重要です。ギフトのビジネスロジックを考えるのではなく、その周辺のインフラ課題を解くのが役目です。
リクエストのルーティング
最初の役割はルータです。Gateway は MCP リクエストを受け取り、その内容・ユーザーコンテキスト・自身の設定に基づいて宛先サービスを選びます。
たとえば GiftGenius では、次のような単純なルーティング表を用意できます。
const TOOL_ROUTES: Record<string, "gift" | "commerce" | "analytics"> = {
suggest_gifts: "gift",
get_similar_gifts: "gift",
checkout_start: "commerce",
get_order_status: "commerce",
log_event: "analytics",
};
そしてこれを使います。
function pickBackend(req: McpRequest, ctx: GatewayContext): Backend {
if (req.method === "tools/list") return "aggregator";
if (req.method === "tools/call") {
const toolName = req.params?.name;
const group = TOOL_ROUTES[toolName] ?? "gift";
return group === "commerce" ? commerceBackend : giftBackend;
}
return giftBackend;
}
ここでの giftBackend、commerceBackend、analyticsBackend は通常の REST サービスです。各サービスにはベース URL("https://gift-api.internal"、"https://commerce-api.internal"、…)があります。Gateway は MCP をそのまま内部にトンネルせず、MCP 呼び出しを適切な REST エンドポイントへの HTTP リクエストに展開します。
境界での認証・認可
第二の重要な役割は、境界の防御です。Gateway は、トークン検証、ユーザーの特定、所属組織、権限(permissions)の把握に最適な場所です。
例えば ChatGPT またはあなたの MCP Auth サーバー発行の OAuth トークンを受け取り、それを検証し(自作の暗号ではなく実績あるライブラリを使うのが望ましい)、整ったコンテキストオブジェクトに変換できます。
type GatewayContext = {
userId: string | null;
tenantId: string | null;
locale: string;
};
function buildContextFromHeaders(req: Request): GatewayContext {
const token = req.headers["authorization"]; // "Bearer ..."
const claims = token ? verifyJwt(token) : null;
return {
userId: claims?.sub ?? null,
tenantId: claims?.tenant ?? null,
locale: (req.headers["x-openai-locale"] as string) || "en-US",
};
}
内部の backend/REST サービスは生の HTTP ヘッダーやトークンの解析に悩む必要がなく、正規化済みの context(userId、tenantId、locale を含む)を受け取れます。MCP ドキュメントの推奨でも、トークン検証を「ゼロから」実装せず、検証済みライブラリと短寿命トークンを使うことが明言されています。
ロギング、トレーシング、メトリクス
第三の役割はオブザーバビリティです。Gateway はすべての着信 MCP リクエストとその応答を観測できます。したがって correlation-id の付与、(機微情報を除いた)ツール引数のログ、応答時間とステータスの記録に最適です。
最も単純なアイデア:
app.use((req, res, next) => {
const requestId = crypto.randomUUID();
(req as any).requestId = requestId;
const start = Date.now();
res.on("finish", () => {
const ms = Date.now() - start;
console.log(
`[${requestId}] ${req.method} ${req.url} -> ${res.statusCode} in ${ms}ms`
);
});
next();
});
このモジュールの観測性の回では、これらのデータを単なる console.log ではなく構造化ストアに送って、ダッシュボードを構築する方法を扱います。
基本的な負荷制御
第四の、しかし重要な役割は一次的な負荷制御です。Gateway で、ユーザー・組織・ツール・エンドポイントごとの呼び出しカウンタを持たせれば、暴走するクライアント 1 つがクラスタやモデル費用を焼き尽くす事態を防げます。
このモジュールではまず考え方のみを押さえます。レート制限やキューは gateway レイヤーに置き、実装の詳細(Redis、トークンバケット、リ―キーバケット)は次回の境界防御の講義で扱います。
リクエストのコンテキスト付加
最後に、gateway は MCP クライアントの生のコンテキストを、内部ツール向けの整った引数に変換する場所としても適しています。
例えば ChatGPT は openai/locale や _meta["openai/userLocation"] でユーザーのロケールや位置情報を渡すことがあります。Gateway は以下を行えます。
- 適切な地域のサービスを選ぶ(ru サーバー、en サーバーなど)。
- ツールの JSON Schema に明示されていなかったとしても、locale をツール引数に追加する(例えば任意フィールドとして)。
一例:
function enrichToolArgs(args: any, ctx: GatewayContext) {
return {
...args,
locale: args.locale ?? ctx.locale,
tenantId: ctx.tenantId,
};
}
その結果、Gift API は「豊かなコンテキスト」を一度に受け取り、例えば "ru-RU" ではロシア語のギフト説明、"en-US" では英語の説明を引き当てることができます。
5. MCP Gateway がやるべきではないこと
「すべてが通る魔法の場所」ができると、以前は別サービスにあったものを何でも入れたくなります。こうして gateway はモンスター化する危険があります。
通常、この層に置くべきではないものがいくつかあります。
第一に、複雑なビジネスロジック。ギフト選定、割引ルール、配送料の計算、ACP のロジック等は、専門の backend/commerce サービス内に留めるべきです。Gateway ができるのは軽い事前バリデーション(例えば価格が負でないかの確認)程度で、SKU の選択や地域別の税計算をすべきではありません。
第二に、長寿命のユーザー状態。Gateway は典型的な stateless サービスであるべきです。水平スケールしやすく、ローカルメモリに依存せず、再起動の影響がないことが重要です。もしチェックアウトウィザードの状態や一時的なカートの中身などを保持し始めると、インスタンス間の同期問題にすぐ苦しむことになります。
第三に、サービス固有の機能。これは Gift API や Commerce API など各 backend サービスの内部に置くのが筋です。例えば、Gift backend が検索結果をキャッシュしたいなら、自身で(必要なら Redis を使って)行うべきです。Gateway はこの内部最適化を知る必要はありません。境界防御の回でも改めて述べますが、ゲートウェイの役割はネットワークとクロスサービス機能であり、推薦のビジネスルールではありません。
第四に、重い計算。Gateway 内で LLM モデルを呼び出したり、複雑な変換・集約を始めたりすると、「軽いフロント」ではなくスケール・デバッグが難しい太ったバックエンドになってしまいます。
6. Gateway、ローカライゼーション、サービスのバージョン
Gateway の基本的な責務と、入れるべきでないものを確認しました。ここではこの層で解くのに適した「やや高度な」課題、すなわちローカライゼーションとサービスのバージョニングを見ます。Gateway のもう一つの面白い役割は、ロケールとサービスバージョンに基づくスマートなルーティングです。
ChatGPT があなたの App を呼ぶ時点で、ユーザーの言語(openai/locale)や、しばしばその位置情報(_meta["openai/userLocation"])は把握済みです。Gateway はこの情報を使って、適切なバックエンドにリクエストを送れます。
例えば「1 つの gateway — 複数の単一言語バックエンド」というアーキテクチャを構築できます。
- ru-Gift API — ロシア語のギフトカタログとテキストのみ。
- en-Gift API — 英語のみ。
- jp-Gift API — 日本語(世界進出する気になったとき)。
この場合、Gateway は ChatGPT に対する MCP サーバーとして振る舞い、locale と userLocation に基づいて適切な内部サービスを選びます。
一例:
function pickGiftBackendByLocale(ctx: GatewayContext): Backend {
if (ctx.locale.startsWith("ru")) return giftRuBackend;
if (ctx.locale.startsWith("ja")) return giftJpBackend;
return giftEnBackend;
}
同じ場所で単純なカナリアルーティングも実装できます。本番アーキテクチャに関するこのモジュールでは、gateway を使って新しいサービスクラスターへ一部のトラフィックだけを送ることを推奨します。
とても粗いカナリアの例:
function pickGiftBackendCanary(ctx: GatewayContext): Backend {
const hash = hashUser(ctx.userId ?? "anonymous");
const bucket = hash % 100;
return bucket < 5 ? giftBackendV2 : giftBackendV1; // トラフィックの 5% を v2 に送る
}
これにより、Gift API の新バージョンを、メトリクスやエラーを見ながら安全に段階的にリリースでき、いきなり本番全体を壊すことがなくなります。
7. 代表的なアーキテクチャ: 「全部入り」から Gateway へ
本講座のこれまででも、ChatGPT App の本番アーキテクチャには複数のパターンがあると見てきました。このモジュールでは 90% のケースに十分な 3 つの基本トポロジに絞ります。
第一は、「全部入り」。App ウィジェット(Next.js)、MCP サーバー、Agents ロジック、簡易な commerce バックエンドが同一サービス(しばしば同一リポジトリ、時に同一 Vercel アプリ)に同居する形です。利点は、ほぼ DevOps が不要、デプロイが簡単、レイテンシ最小。欠点は、部分的なスケールが難しく、1 つのホットな機能がアプリ全体を落としうること、コンポーネント間の境界が曖昧なことです。
第二は、App + MCP Gateway + 複数のバックエンドサービス。ここでは Next.js ウィジェットは(例: Vercel に)分離し、すべての MCP トラフィックは Gateway を経由して Gift REST API、Commerce REST API、Agents サービス、ACP バックエンドなどへルーティングされます。ちょうど今 GiftGenius を通して扱っている構成で、90% の実運用ケースに適しています。
第三は同じ構成を複数地域(multi‑region)で展開し、gateway の前にグローバルロードバランサを置く形です。欧州のユーザーは eu クラスタ、米国のユーザーは us クラスタに入り、各地域は「Gateway + 複数バックエンドサービス」の構成で構築します。これはグローバルなオーディエンスを抱える比較的大規模プロジェクト向けの話です。
今重要なのはすべてのバリエーションを暗記することではなく、小さな段階で MCP モノリスや App のバックエンドがその役割を果たしていたとしても、gateway をアーキテクチャの独立した論理コンポーネントとして考えられるようになることです。
8. MCP Gateway はどこに置くのか
朗報です。MCP Gateway は必ずしも Kubernetes 上の巨大な専用サービスである必要はありません。多くの場合、いくつかの成長段階をたどります。
最小スケールでは、MCP サーバー自体が gateway の役割を果たせます。この場合はコードの構造化に気を配るだけです。ルーティング、認証、ログを 1 つのモジュールに、ツールのロジックを別のモジュールに分けます。このモジュールでは、小規模システムでは gateway の機能が MCP サーバーや App のバックエンド(例: Next.js の API route)内にあって良いことを明記しています。
次のステップは、独立した Node/TypeScript サービスです。"/mcp" を待ち受け、内部の複数 HTTP サービスへアクセスする Express/Fastify アプリなどです。多くのチームにとってこれは馴染み深い DevOps ツール群に乗せやすく、快適な選択です。
この種のサービスの最小スケルトン:
const app = express();
app.use(express.json());
app.post("/mcp", handleMcpRequest); // ここに gateway のロジックが集約される
app.listen(4000, () => {
console.log("MCP Gateway listening on :4000");
});
さらに成熟した段階では、AWS API Gateway、Cloudflare Workers/Routes、NGINX/Envoy(ルーティング設定と Lua/JS スクリプト)などのマネージド解に実装を載せることもできます。重要なのは、これは実装の変更であって概念の変更ではないことです。アーキテクチャ的には ChatGPT は依然として単一点にアクセスし、詳細は gateway が処理します。
9. ミニ例: GiftGenius 用のシンプルな MCP Gateway
ルーティング、コンテキスト、tools/list の扱いを個別に見ました。ここではそれらを 1 つの小さく明快な例にまとめます。内部には 2 つの REST サービスがあるとします。
- GIFT_API_BASE = "https://gift-api.internal";
- COMMERCE_API_BASE = "https://commerce-api.internal"。
そして ChatGPT は "https://gateway.giftgenius.com/mcp" にある 1 つの gateway を叩きます。
まず型をいくつか定義します。
type Backend = "gift" | "commerce";
type ToolRoute = {
backend: Backend;
method: "GET" | "POST";
path: string;
};
const TOOL_ROUTES: Record<string, ToolRoute> = {
suggest_gifts: {
backend: "gift",
method: "POST",
path: "/api/gifts/suggest",
},
checkout_start: {
backend: "commerce",
method: "POST",
path: "/api/checkout/start",
},
get_order_status: {
backend: "commerce",
method: "GET",
path: "/api/orders/status",
},
};
次に backend の選択と呼び出しを実装します。
async function callBackend(toolName: string, mcpReq: McpRequest, ctx: GatewayContext) {
const route = TOOL_ROUTES[toolName];
if (!route) {
throw new Error(`Unknown tool: ${toolName}`);
}
const base =
route.backend === "gift" ? GIFT_API_BASE : COMMERCE_API_BASE;
const url = base + route.path;
// tools/call の MCP 呼び出しで渡ってきた args
const args = {
...(mcpReq.params?.arguments ?? {}),
locale: ctx.locale,
};
const res = await fetch(url, {
method: route.method,
headers: { "content-type": "application/json" },
body: route.method === "POST" ? JSON.stringify(args) : undefined,
});
const data = await res.json();
// REST サービスの応答を MCP の応答形式にラップする
return {
result: data,
} satisfies McpResponse;
}
最後に、次のことを行うメインハンドラです。
- ヘッダーからコンテキストを構築する(認証、ロケール)。
- バックエンドを選択する。
- tools/list は集約し、tools/call はプロキシする。
app.post("/mcp", async (req, res) => {
const mcpReq = req.body as McpRequest;
const ctx = buildContextFromHeaders(req);
if (mcpReq.method === "tools/list") {
// Gateway 自身がツールとそのスキーマを宣言する
const tools = [
{
name: "suggest_gifts",
description: "予算と興味に基づいてギフトを提案します。",
inputSchema: { /* ... JSON Schema ... */ },
},
{
name: "checkout_start",
description: "注文の下書きを作成し、チェックアウトを開始します。",
inputSchema: { /* ... */ },
},
// ...
];
return res.json({ result: { tools } });
}
if (mcpReq.method === "tools/call") {
const toolName = mcpReq.params?.name;
const backendRes = await callBackend(toolName, mcpReq, ctx);
return res.json(backendRes);
}
res.status(400).json({ error: { message: "Unsupported MCP method" } });
});
これはもちろん簡略化した構成ですが、すでに重要なアイデアを示しています。
- gateway は Gift API がどのようにギフトを選ぶかを知らない。
- 引数を丁寧に付加し、ルーティングし、必要ならログやレート制限を行うだけです。
10. これらがモジュール以降のトピックとどう関係するか
MCP Gateway は、このモジュール残りの講義全体の土台となります。
- 次回は境界防御(レート制限、キュー、バックプレッシャ)を扱います。これらはまず gateway レベルに実装します。というのも、gateway こそが全着信トラフィックを観測し、バックエンドが飽和する前に「余分を切れる」場所だからです。
- 次にレジリエンス(タイムアウト、サーキットブレーカ、バルクヘッド)を議論します。Gateway は外部呼び出しのタイムアウトを集中管理したり、問題のあるサービスをオン/オフしたり(エラーが多い間だけ Commerce API を一時的に切る、など)するのに最適です。
- そしてスケーリングとデプロイの回では、gateway を独立したクラスタとして見ます。ロードバランシング、blue/green・canary のリリース、内部 MCP サービス群から独立したロールバックが可能です。
要するに、以前は「App と MCP サーバーがある」と考えていたものが、「App、MCP Gateway、複数の backend/Agents クラスタ、commerce バックエンドがある」という構成に拡張されます。それでも ChatGPT 側の設定を複雑にしないのは gateway のおかげで、MCP の接続先は依然として 1 箇所だけに見えるのです。
11. MCP Gateway でよくあるミス
誤り No.1: gateway を「ビジネスモンスター」にする。
よくある落とし穴です。すべてが gateway を通るのだから、割引計算、SKU 選定、複雑なカテゴリルールやプロモコード検証も入れてしまおう、と考えてしまう。結果としてスケールも変更も難しい太ったサービスになり、Gift API、Commerce API、その他の専門コンポーネントに分けた意味を失います。Gateway は薄いネットワーク層に留め、ドメイン固有のものは専門サービスの中に保ちましょう。
誤り No.2: gateway に長寿命のユーザー状態を保持する。
「ユーザーのカートを gateway のメモリに覚えさせよう」という発想は、インスタンスが 1 つの間は魅力的に見えます。しかし 2 つ目が出た瞬間に、真のカートは A と B のどちらにあるのか、再起動後はどうなるのか、といった苦痛が始まります。Gateway は stateless を維持すべきです。せいぜいハンドシェイクや設定の小さなキャッシュまで。セッションや注文の状態は DB や専門サービスに置きます。
誤り No.3: ChatGPT を内部トポロジに詳しくさせる。
ChatGPT に複数の API サーバー(個別の Gift API、個別の Commerce API)を直接見せ、gateway を「部分的に」使い始めると、単一入口と集中管理という最大の利点を失います。トポロジを変えるたびに複数箇所の設定を直す羽目になります。App の公式エンドポイントとして MCP Gateway を 1 つ設定し、内部の変化はその背後に隠す方がずっと簡単です。
誤り No.4: クロスサービスのロジックを全バックエンドに重複実装する。
認証、レート制限、ログ、ローカライゼーションを各 REST サービス内で個別実装しようとするチームがあります。結果として権限や制限のポリシーが Gift API と Commerce API で食い違い、App のふるまいが予測不能になります。Gateway はまさにこれらを集中させるためのものです。トークンを検証し、テナントとロケールを決め、呼び出しを記録し、制限を適用してから、具体的なサービスへ進みます。
誤り No.5: gateway を重い計算や LLM 呼び出しで過負荷にする。
技術的には gateway からさらに LLM モデルを呼び出したり、複雑な集計や長いバッチ処理を行ったりできます。しかしそれではスケーラブルにも分離可能にもならない「もう一つの重いバックエンド」になってしまいます。Gateway は高速で予測可能なままであるべきです。せいぜい軽い変換とルーティングまで。重い処理は REST サービス内部か、次回扱うキュー/ワーカーへ。
誤り No.6: 早すぎるインフラの複雑化。
逆の極端もあります。小さな学習用 App に対して、いきなり独立した Kubernetes クラスタ、NGINX スタック、Cloudflare Workers、複雑な設定群をこしらえることです。実際の負荷や堅牢性要件が出るまでは意味がありません。単一の MCP モノリスやシンプルな Node 製 gateway から始め、成長に応じてコンポーネントをクラスタやマネージドサービスに切り出していくのが健全です。
GO TO FULL VERSION