1. なぜローカリゼーションのアーキテクチャを考えるべきか
言語がひとつでカタログも小さいうちは簡単です。gift_catalog.json を保存し、文言はすべてロシア語、MCP サーバは誰に対してもそのギフトをそのまま返す——そんな作りで十分です。ところが、次のような要件が出てくると状況が変わります。
- 米国やヨーロッパ向けの英語 UI、
- マトリョーシカやロシア語の書籍を含むロシア語カタログ、
- 市場ごとの違い(米国は Amazon、ロシアは Ozon など)、
各ハンドラに「if (locale === "ru") をもうひとつ」——という素朴なやり方は、コードを「クリスマスツリー」状態にしてしまいます。
MCP は一方でプロトコル、他方でそのプロトコルのサーバ実装です。サーバは ChatGPT からのリクエストを受け取り、locale や userLocation といったメタデータも一緒に受け取ります。重要なのは「locale を読めるかどうか」ではなく、アーキテクチャのどこでそのシグナルを考慮するかです。各ツールで行ってもよいし、ロジックの一部を Gateway という別層に切り出すこともできます。
よいローカリゼーション設計は次の三つの問いに答えます。
- どこで使用言語と地域を決定するか。
- どこで必要なデータや統合先(カタログ、ショップ API、通貨など)を選ぶか。
- どこで、またどのようにユーザーの状態(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 つ。各ツールの内部では次を行います。
- _meta または引数から locale を取得する。
- locale に基づいて適切なリソース(gift_catalog.en.json、gift_catalog.ru.json など)を選ぶ。
- 要求された言語で結果を返す。
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 を考慮する場所 | ツールのコード内 |
| デプロイとスケーリング | 容易(単一点) |
| カタログのローカライズ | 条件付きでファイル/リクエストを切替 |
| if(locale ...)の分岐コード量 | 増えがち |
| 異なる市場/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 モノリス」にしないことが大切です。本講義での役割はかなり限定します。
- ChatGPT から MCP メッセージを受け取る(_meta を含む)。
- locale / userLocation を取り出す。
- それに基づいてバックエンドサーバを選ぶ。
- (JSON‑RPC で)選んだ先へプロキシし、レスポンスを返す。
どのギフトカタログを使うか、Amazon と Ozon をどう呼び分けるかといった判断は、各言語 MCP サーバ内に閉じ込めます。Gateway は「理想の義母向けギフト」を知る必要はありません。ru-RU なら mcp-giftgenius-ru、en-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 は listTools や listResources といった 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);
}
);
このように、一度 clientId → locale/country の対応を「記憶」しておけば、以後のツール呼び出しで毎回引数にコピーする必要はありません。
同様に Gateway は希望通貨や価格表記など、コマースロジックに役立つ設定も覚えられます(詳細は ACP のモジュールで扱います)。
7. GiftGenius: 2 つのシナリオとアーキテクチャ選択の影響
抽象的な図形の話に終始しないよう、GiftGenius の具体例で見てみましょう。
シナリオ1: ロシア在住のユーザーがロシア語で書く
前提:
- _meta["openai/locale"] = "ru-RU"
- _meta["openai/userLocation"].country = "RU"
ユーザーの入力例: 「同僚向けのプレゼントを選んで。ボードゲームが好き。予算は3,000ルーブルまで。」
モデル1(単一 MCP)の場合:
- ハンドラは _meta から locale を読み取り、"ru-RU" を得る。
- gift_catalog.ru.json を読み込む(名称はロシア語、通貨はルーブル)。
- カテゴリと予算でフィルタし、ロシア語の構造化ギフト一覧を返す。
モデル2(Gateway + 単一言語サーバ)の場合:
- Gateway は locale と userLocation を読み取り、RU ユーザーだと判断する。
- suggest_gifts を mcp-giftgenius-ru にルーティングする。
- このサーバはロシア語カタログと 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-en に country=DE を渡して呼ぶ、
- またはヨーロッパ向けの mcp-giftgenius-eu を別に用意する。
ここから明らかなように、ロケール(言語)と地域(userLocation)は別次元であり、それらを統合して「どのサービスを呼び、何の商品を見せるか」を決める場所として Gateway は適しています。
8. ツールスキーマに locale を含めるか、_meta のみを使うか
単一 MCP か、Gateway + 単一言語サービスかに関わらず、最後に重要なポイントを議論します。locale を _meta だけに依存させるか、ツールの引数としても持たせるかという点です。
アプローチは 2 つあります。
第一: _meta のみを頼りにする。
この方法の利点は、ツールスキーマが余計なフィールドで散らからないことです。サーバは extra._meta から locale を読み取り、自律的に判断します。モデル1ではこれで十分なことが多いです。
第二: inputSchema に locale(必要なら 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/locale と openai/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"] を使わず、あらゆるパラメータを毎回引数で重複させる。
ツール引数に毎回 locale、country、currency、userId ……と半分の UI を詰め込むと、すぐにつらくなります。MCP は匿名ユーザー ID を _meta["openai/subject"] で渡しているので、Gateway やバックエンド側のクライアント状態にそれらを保持できます。これによりコントラクトが簡潔になり、引数の不整合リスクも減ります。
エラー6: 進化の戦略がない(いきなり 10 言語 Gateway を作る)。
「最初から完璧に」——Gateway、5 言語、3 地域、10 サービス……とやりたくなりますが、実際には「単一 MCP + locale パラメータ(あるいは _meta)」で始め、挙動を安定させてから Gateway と単一言語サービスに切り出していく方が簡単です。最初から巨大な「動物園」を作ろうとすると、リリースが遅れ、デバッグも難しくなるのが通例です。
GO TO FULL VERSION