CodeGym /コース /ChatGPT Apps /MCP Gateway とローカリゼーション設計: 単一言語サーバ、locale をパラメータとして扱う、クライア...

MCP Gateway とローカリゼーション設計: 単一言語サーバ、locale をパラメータとして扱う、クライアント状態

ChatGPT Apps
レベル 9 , レッスン 4
使用可能

1. なぜローカリゼーションのアーキテクチャを考えるべきか

言語がひとつでカタログも小さいうちは簡単です。gift_catalog.json を保存し、文言はすべてロシア語、MCP サーバは誰に対してもそのギフトをそのまま返す——そんな作りで十分です。ところが、次のような要件が出てくると状況が変わります。

  • 米国やヨーロッパ向けの英語 UI、
  • マトリョーシカやロシア語の書籍を含むロシア語カタログ、
  • 市場ごとの違い(米国は Amazon、ロシアは Ozon など)、

各ハンドラに「if (locale === "ru") をもうひとつ」——という素朴なやり方は、コードを「クリスマスツリー」状態にしてしまいます。

MCP は一方でプロトコル、他方でそのプロトコルのサーバ実装です。サーバは ChatGPT からのリクエストを受け取り、localeuserLocation といったメタデータも一緒に受け取ります。重要なのは「locale を読めるかどうか」ではなく、アーキテクチャのどこでそのシグナルを考慮するかです。各ツールで行ってもよいし、ロジックの一部を Gateway という別層に切り出すこともできます。

よいローカリゼーション設計は次の三つの問いに答えます。

  1. どこで使用言語と地域を決定するか。
  2. どこで必要なデータや統合先(カタログ、ショップ API、通貨など)を選ぶか。
  3. どこで、またどのようにユーザーの状態(locale、通貨、好みなど)を保持し、毎回手で引数に入れなくても済むようにするか。

本講義ではこれらを順に見ていきます。

2. MCP、_meta とステートレス性: なぜ locale は明示的に渡すべきか

アーキテクチャのどこで locale を考慮するかを決める前に、MCP リクエストがプロトコルレベルでどう見えるのか、そしてプラットフォームがすでにどんなメタデータを渡してくれるのかを思い出しておきましょう。

重要なポイントを確認します。MCP のリクエストは JSON‑RPC メッセージです。各メッセージは独立しており、プロトコルはステートフルなセッションを前提にしていません。したがって、サーバがロケールを考慮すべきなら、次のいずれかが必要です。

  • ツールの引数として明示的に渡す(inputSchema 内の locale)、または
  • ChatGPT がリクエストに付与する _meta["openai/locale"] から読む。

_meta から locale を読む最小のハンドラ例:

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    inputSchema: { /* ... */ },
  },
  async (args, extra) => {
    const meta = extra?._meta ?? {};
    const locale = (meta["openai/locale"] as string | undefined) || "en-US";
    const country = meta["openai/userLocation"]?.country as string | undefined;

    // 以降は locale と country を使ってカタログを選ぶ
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

ここでは locale を引数で渡さず、SDK が extra に入れてくれた _meta に依存しています。これは十分に実用的で、最初のモデル(単一の多言語 MCP)で役立ちます。

二つ目のモデル(Gateway を用いる構成)でも _meta は鍵となります。Gateway はメタデータから locale を読み取り、それに基づいてどこにルーティングするかを決めます。 locale をどの形式で保持するか——_meta のみにするか、ツールスキーマにも含めるか——は後ほど別のブロックで扱います。

3. モデル1: 単一の多言語 MCP サーバ(「ポリグロット・モノリス」)

最もシンプルな構成から始めましょう。MCP サーバは 1 台、URL もデプロイもコードベースも 1 つ。各ツールの内部では次を行います。

  1. _meta または引数から locale を取得する。
  2. locale に基づいて適切なリソース(gift_catalog.en.jsongift_catalog.ru.json など)を選ぶ。
  3. 要求された言語で結果を返す。

GiftGenius の例

たとえば、次の 2 ファイルがあるとします。

  • data/gift_catalog.en.json
  • data/gift_catalog.ru.json

必要なファイルを選ぶ小さなヘルパー loadGiftCatalog(locale) を用意します。

async function loadGiftCatalog(locale: string) {
  const lang = locale.split("-")[0]; // "ru-RU" → "ru"
  const fileName = lang === "ru" ? "gift_catalog.ru.json" : "gift_catalog.en.json";
  const data = await import(`../data/${fileName}`);
  return data.default; // ギフトの配列
}

あとはツール suggest_gifts からこのヘルパーを呼ぶだけです。

server.registerTool(
  "suggest_gifts",
  { title: "ギフトの選定", inputSchema: {/* ... */} },
  async (args, extra) => {
    const locale = (extra?._meta?.["openai/locale"] as string) || "en-US";
    const catalog = await loadGiftCatalog(locale);
    const filtered = filterGifts(catalog, args);
    return { structuredContent: { gifts: filtered } };
  }
);

このようにローカリゼーションは loadGiftCatalog に集約され、ツールは locale を渡すだけになります。日付や通貨などの地域依存の要素も同様に切り替えられます。

このモデルの長所と短所

長々と説明しすぎないよう、まずはモデル1(単一 MCP)に限って長所と短所を表にまとめます。Gateway との比較は後ほど取り上げます。

評価項目 単一の多言語 MCP
MCP インスタンス数 1
locale を考慮する場所 ツールのコード内
デプロイとスケーリング 容易(単一点)
カタログのローカライズ 条件付きでファイル/リクエストを切替
iflocale ...)の分岐コード量 増えがち
異なる市場/API のサポート 多様な要件が 1 つのコードに集約

このモデルが向いているのは次のような場合です。

  • MVP や小規模アプリ(言語が 2–3 程度で市場差が小さい)、
  • 学習用プロジェクト(本講座の GiftGenius など)。

逆に次のような場合は向きません。

  • サポート言語が増えてきた、
  • 市場ごとにチームやデータが大きく異なる(専用 DB、e‑commerce API、法規対応など)。

こういう場面で第二のモデルが活きてきます。

4. モデル2: MCP Gateway + 単一言語のバックエンドサーバ

GiftGenius が米国・ロシア・ドイツで動くとしましょう。米国は Amazon API、ロシアは Ozon、ドイツは現地の小売 API。市場ごとに契約も仕様もチームも違う——すべてを単一 MCP モノリスに押し込むのは快適ではありません。

モデル2の考え方はこうです。

ChatGPT と実際の MCP サービスの間に Gateway を置きます。ChatGPT から見ると 1 台の MCP サーバですが、中ではリクエストを各バックエンドサーバにルーティングします。各バックエンドは「1 言語・1 市場」だけを扱います。

図にするとこうなる

まず 2 つのモデルを図で比較します。

flowchart LR
    subgraph Model1["モデル1: 単一の MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (多言語)"]
    end

    subgraph Model2["モデル2: Gateway + モノ"]
      A2[ChatGPT] --> G[MCP Gateway]
      G --> R["GiftGenius MCP RU (ru-RU, Ozon)"]
      G --> E["GiftGenius MCP EN (en-US, Amazon"]
      G --> D["GiftGenius MCP DE (de-DE, Local shop)"]
    end

モデル2では、ChatGPT の視点からは MCP のエンドポイントは Gateway ひとつだけです。内部では _meta["openai/locale"] や _meta["openai/userLocation"] を解析し、適切なバックエンドを選びます。

Gateway が担うこと(本講義の範囲)

Gateway を「ビジネスロジック満載の第 2 モノリス」にしないことが大切です。本講義での役割はかなり限定します。

  1. ChatGPT から MCP メッセージを受け取る(_meta を含む)。
  2. locale / userLocation を取り出す。
  3. それに基づいてバックエンドサーバを選ぶ。
  4. (JSON‑RPC で)選んだ先へプロキシし、レスポンスを返す。

どのギフトカタログを使うか、Amazon と Ozon をどう呼び分けるかといった判断は、各言語 MCP サーバ内に閉じ込めます。Gateway は「理想の義母向けギフト」を知る必要はありません。ru-RU なら mcp-giftgenius-ruen-US なら mcp-giftgenius-en に送れば十分です。

TypeScript による MCP Gateway の最小スケルトン

詳細に踏み込みすぎないよう、大幅に簡略化します。内部 MCP サーバと JSON‑RPC でやり取りできるヘルパー callDownstreamTool があると仮定します(HTTP でも SSE 常時接続でもよいですが、詳細はモジュール16に譲ります)。

import { Server } from "@modelcontextprotocol/sdk/server";

const server = new Server({ name: "giftgenius-gateway" });

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // デフォルト
  const lang = locale.split("-")[0];     // ru-RU → ru
  return ["ru", "de"].includes(lang) ? lang : "en";
}

server.registerTool(
  "suggest_gifts",
  { title: "Suggest gifts (via gateway)", inputSchema: {/* ... */} },
  async (args, extra) => {
    const locale = extra?._meta?.["openai/locale"] as string | undefined;
    const backendKey = chooseBackend(locale); // "ru" | "en" | "de"
    // 適切なバックエンドサーバ上の同名ツールを呼び出す
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

内部の MCP サーバ群は同じ契約で suggest_gifts を公開しますが、それぞれが自分の言語・市場だけを扱い、他言語の存在を知りません。

同じ要領で Gateway は listToolslistResources といった MCP メソッドもプロキシできますが、これらは別モジュールの話題です。

5. ローカリゼーションのための 2 モデル比較

先に「単一 MCP」モデルの長所・短所を見ました。ここでは主要な観点で両モデルの違いをまとめます。

評価項目 単一の多言語 MCP Gateway + 単一言語 MCP サーバ
MCP サービス数 1 Gateway 1 + バックエンド N
locale を考慮する場所 各ツール内(if locale ... のロジック) Gateway でルーティング。各サービスは固定言語
UX の柔軟性(言語切替) 容易(単一箇所で完結。LLM は locale を変えるだけ) 可能だが、Gateway の切替設計が必要
インフラの複雑さ 最小 高い(言語ごとに個別デプロイ)
市場ごとの分離 低い(単一コード・単一プロセス) 高い(RU が落ちても EN には影響しない 等)
チーム分担のしやすさ 責務分離が難しい 自然に分担可能(RU/EN/DE チームが別々に開発)
ローカリゼーションのロジックの所在 各ハンドラのビジネスロジックと混在 Gateway と各バックエンドの境界に整理される

本講座では主にモデル1(単一 MCP + locale をパラメータとして扱う)を採用し、Gateway モデルは多市場展開にスケールする自然な次の一歩として扱います。 ただし Gateway は次の一歩として有力なので、ここでは重要なディテール——セッション状態に locale と国情報をどう持たせるか——を見ておきます。

6. Gateway におけるクライアント状態の一部としての locale

ここまでは、各リクエストが必要情報をすべて含む前提で話しました。しかし実運用では、一部の情報をセッション状態として保持できると便利です。例えば:

  • ある時点でユーザーが locale = "ru-RU"userLocation.country = "RU" だった。
  • 以後は、引数に明示的な locale がなくても、そのユーザーの呼び出しは常に RU バックエンドにルーティングしたい。

MCP には便利なフィールド _meta["openai/subject"] があります。OpenAI が匿名のユーザー識別子として送るもので、これをセッションキーにできます。

メモリ内での簡易状態実装

Gateway に極小の状態レイヤを実装してみます(もちろん本番では Map の代わりに Redis などの外部ストアを使います)。

type ClientState = {
  locale?: string;
  country?: string;
};

const clientState = new Map<string, ClientState>();

function getClientId(extra: any): string | undefined {
  return extra?._meta?.["openai/subject"] as string | undefined;
}

function updateClientState(extra: any) {
  const clientId = getClientId(extra);
  if (!clientId) return;

  const meta = extra?._meta ?? {};
  const current = clientState.get(clientId) ?? {};
  const next: ClientState = {
    locale: meta["openai/locale"] || current.locale,
    country: meta["openai/userLocation"]?.country || current.country,
  };
  clientState.set(clientId, next);
}

これで Gateway のハンドラでは、まず状態を更新し、その後バックエンド選択に使えます。

server.registerTool(
  "suggest_gifts",
  { title: "Suggest gifts (via gateway)", inputSchema: {/* ... */} },
  async (args, extra) => {
    updateClientState(extra);
    const clientId = getClientId(extra)!;
    const state = clientState.get(clientId);
    const locale = state?.locale || "en-US";

    const backendKey = chooseBackend(locale);
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

このように、一度 clientIdlocalecountry の対応を「記憶」しておけば、以後のツール呼び出しで毎回引数にコピーする必要はありません。

同様に Gateway は希望通貨や価格表記など、コマースロジックに役立つ設定も覚えられます(詳細は ACP のモジュールで扱います)。

7. GiftGenius: 2 つのシナリオとアーキテクチャ選択の影響

抽象的な図形の話に終始しないよう、GiftGenius の具体例で見てみましょう。

シナリオ1: ロシア在住のユーザーがロシア語で書く

前提:

  • _meta["openai/locale"] = "ru-RU"
  • _meta["openai/userLocation"].country = "RU"

ユーザーの入力例: 「同僚向けのプレゼントを選んで。ボードゲームが好き。予算は3,000ルーブルまで。」

モデル1(単一 MCP)の場合:

  1. ハンドラは _meta から locale を読み取り、"ru-RU" を得る。
  2. gift_catalog.ru.json を読み込む(名称はロシア語、通貨はルーブル)。
  3. カテゴリと予算でフィルタし、ロシア語の構造化ギフト一覧を返す。

モデル2(Gateway + 単一言語サーバ)の場合:

  1. Gateway は locale と userLocation を読み取り、RU ユーザーだと判断する。
  2. suggest_giftsmcp-giftgenius-ru にルーティングする。
  3. このサーバはロシア語カタログと Ozon API のみを扱い、ルーブル建てでギフトを返す。

どちらのモデルでも、ユーザーは母語で結果を得られますが、後者では英語用 MCP サーバはロシア市場向けカタログの存在すら知りません。

シナリオ2: ドイツ在住のユーザーが英語で書く

前提:

  • _meta["openai/locale"] = "en"
  • _meta["openai/userLocation"].country = "DE"

ユーザーの入力例: “Gift for my German coworker, budget 50 EUR”.

モデル1の場合:

  • locale "en" により英語テキストを返す。
  • country "DE" を使って、ユーロ建て・欧州向けのカタログを選べる。

モデル2の場合:

  • Gateway は、locale = "en" なので英語サービス、country = "DE" なので欧州在庫、といった判断を組み合わせます。ビジネスロジックに応じて次のようにできます。
  • mcp-giftgenius-encountry=DE を渡して呼ぶ、
  • またはヨーロッパ向けの mcp-giftgenius-eu を別に用意する。

ここから明らかなように、ロケール(言語)と地域(userLocation)は別次元であり、それらを統合して「どのサービスを呼び、何の商品を見せるか」を決める場所として Gateway は適しています。

8. ツールスキーマに locale を含めるか、_meta のみを使うか

単一 MCP か、Gateway + 単一言語サービスかに関わらず、最後に重要なポイントを議論します。locale を _meta だけに依存させるか、ツールの引数としても持たせるかという点です。

アプローチは 2 つあります。

第一: _meta のみを頼りにする。

この方法の利点は、ツールスキーマが余計なフィールドで散らからないことです。サーバは extra._meta から locale を読み取り、自律的に判断します。モデル1ではこれで十分なことが多いです。

第二: inputSchemalocale(必要なら currency も)を明示的に含める。

const suggestGiftsSchema = {
  type: "object",
  properties: {
    locale: {
      type: "string",
      description: "User locale in BCP 47 format, e.g. en-US or ru-RU"
    },
    recipient: { type: "string" },
    // ...
  },
  required: ["recipient"]
};

さらに system‑prompt で、モデルに対しユーザーコンテキストから得た値を使って常に locale を引数に入れるよう指示できます。これにより意図が明確になります。JSON の引数を見れば、サーバがどの言語で動作すべきかが一目瞭然です。とくに「共通 MCP が locale によって内部ルーティング先を切り替える」といった複雑なアーキテクチャでは有用です。

実際には両者を併用することが多いです。スキーマに locale フィールドを設けつつ、もしモデルが埋め損ねたら _meta["openai/locale"] でフォールバックする、という形です。

9. Gateway におけるローカリゼーションと「余計なロジック」の境界

陥りやすい罠として、次のようなものがあります。

  • Gateway 自身が表示するギフトを決めてしまう、
  • 日付や価格のフォーマットまで Gateway が行う、
  • クリックの集計レポートまで Gateway が作る、など。

これは魅力的に聞こえますが、Gateway を「第 2 のモノリス」にしてしまい、更新や運用を難しくします。業界の API ゲートウェイ実務(MCP Gateway も役割は同様)では、認証・認可・ルーティング・軽いコンテキスト付与にフォーカスします。たとえば、ゲートウェイが HTTP ヘッダーを便利なメタデータに変換する、といった程度です。ビジネスロジックや重い処理はバックエンドに置くべきです。

ローカリゼーションの観点では、次のような方針になります。

  • Gateway は _meta["openai/locale"] と _meta["openai/userLocation"] をパースしてよい。
  • それらをクライアント状態に保存してよい。
  • 適切な言語サーバを選ぶ、またはリクエストに locale/country を付加してよい。

ただし、ギフトの選定、年齢や予算によるフィルタリングなどは MCP バックエンド側に置いておきます。

10. MCP と Gateway でローカリゼーションを設計する際のよくある失敗

エラー1: ユーザー文からの言語推定のみに頼る。
メッセージ本文を言語判定にかけ、その結果だけでどのサーバを呼ぶか決めたくなることがあります。フォールバックとしては有用ですが、主要メカニズムにすべきではありません。プラットフォームは openai/localeopenai/userLocation をすでに提供しており、ChatGPT の設定やユーザー環境を反映しています。これらのシグナルを無視して「言語当てクイズ」をするのは、思わぬところで UX を壊す近道です。

エラー2: locale をモデルの「頭の中」にだけ保持し、サーバに渡さない。
locale_meta にもツール引数にも現れなければ、サーバはユーザーの言語を知りません。モデルが「книги」を books に訳そうとするかもしれませんが、カテゴリが複雑だと当てになりません。正しいやり方は locale を明示的に渡すことです。ツール引数の locale か、_meta から読み、それを前提にアーキテクチャを組み立てます。

エラー3: ローカリゼーションのビジネスロジックをすべて Gateway に移す。
Gateway がギフト選定を行い、DB にアクセスし、外部 API と格闘し始めると、軽量なルータではなく重いサービスになり、スケールも更新も難しくなります。結果としてモノリスが 2 つになるだけです。Gateway はできるだけ「賢くしすぎない」ようにしましょう。locale/userLocation を見てバックエンドを選び、メタデータを丁寧に中継する程度に留めます。

エラー4: ルーティングを IP や userLocation のみに固定する。
「国が RU なら RU サーバへ」という単純化は魅力的ですが、ドイツ在住でもロシア語 UI を望むユーザーがいたり、セッション途中で「switch to English」と言われることもあります。openai/locale やユーザーの言語変更の意思を Gateway で考慮しないと、ルーティングが「固定化」され、UX を損ないます。locale と userLocation の組み合わせに基づき、セッション状態で上書えも可能にしておくのがよいでしょう。

エラー5: _meta["openai/subject"] を使わず、あらゆるパラメータを毎回引数で重複させる。
ツール引数に毎回 localecountrycurrencyuserId ……と半分の UI を詰め込むと、すぐにつらくなります。MCP は匿名ユーザー ID を _meta["openai/subject"] で渡しているので、Gateway やバックエンド側のクライアント状態にそれらを保持できます。これによりコントラクトが簡潔になり、引数の不整合リスクも減ります。

エラー6: 進化の戦略がない(いきなり 10 言語 Gateway を作る)。
「最初から完璧に」——Gateway、5 言語、3 地域、10 サービス……とやりたくなりますが、実際には「単一 MCP + locale パラメータ(あるいは _meta)」で始め、挙動を安定させてから Gateway と単一言語サービスに切り出していく方が簡単です。最初から巨大な「動物園」を作ろうとすると、リリースが遅れ、デバッグも難しくなるのが通例です。

1
アンケート/クイズ
ローカリゼーション、レベル 9、レッスン 4
使用不可
ローカリゼーション
ローカリゼーション(UI、データ、機能の説明)
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION