CodeGym /コース /ChatGPT Apps /MCP Gateway: なぜ ChatGPT と自社サービスの間に必要なのか

MCP Gateway: なぜ ChatGPT と自社サービスの間に必要なのか

ChatGPT Apps
レベル 16 , レッスン 0
使用可能

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_giftscheckout_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 メソッドを呼び出します(fetchaxios など)。

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;
}

ここでの giftBackendcommerceBackendanalyticsBackend は通常の 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 ヘッダーやトークンの解析に悩む必要がなく、正規化済みの contextuserIdtenantIdlocale を含む)を受け取れます。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 サーバーとして振る舞い、localeuserLocation に基づいて適切な内部サービスを選びます。

一例:

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;
}

最後に、次のことを行うメインハンドラです。

  1. ヘッダーからコンテキストを構築する(認証、ロケール)。
  2. バックエンドを選択する。
  3. 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 から始め、成長に応じてコンポーネントをクラスタやマネージドサービスに切り出していくのが健全です。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION