CodeGym /課程 /ChatGPT Apps /MCP Gateway 與在地化架構:單語伺服器、以 locale 為參數、用戶端狀態

MCP Gateway 與在地化架構:單語伺服器、以 locale 為參數、用戶端狀態

ChatGPT Apps
等級 9 , 課堂 4
開放

1. 為什麼要思考在地化的架構

當你只有一種語言和小型目錄時,一切都很簡單:你保存 gift_catalog.json,所有文字都是俄文,而 MCP 伺服器就把這些禮物如實回給所有人。但只要你想要:

  • 面向美國與歐洲的英文 UI,
  • 一份獨立的俄語目錄,包含套娃與俄文書籍,
  • 不同市場(美國用 Amazon,俄羅斯用 Ozon),

天真的做法——在每個 handler 裡再加一個 iflocale === "ru")——很快就把程式碼變成一棵聖誕樹。

MCP 一方面是協定,另一方面是該協定的伺服器實作。伺服器會從 ChatGPT 收到請求與中繼資料,其中包含 localeuserLocation。關鍵不在於「它會不會讀 locale」,而在於你在架構中的哪一層消化這個訊號。可以在每個工具內都處理,也可以把部分邏輯抽到獨立的一層——Gateway。

良好的在地化架構應回答三個問題:

  1. 我們在哪裡決定要用哪個語言與地區。
  2. 我們在哪裡選擇所需的資料與整合(目錄、店家 API、貨幣)。
  3. 我們在哪裡、以及如何保存使用者狀態(locale、貨幣,或其他偏好),以免每次都要手動傳遞。

今天就來拆解這些。

2. MCP、_meta 與無狀態特性:為什麼 locale 必須顯式傳遞

在決定要把 locale 放在哪一層處理之前,先回顧 MCP 請求在協定層的樣貌,以及平台已經替你傳了哪些中繼資料。

提醒一個關鍵事實:MCP 請求是 JSON‑RPC 訊息。每個訊息都是獨立的,協定不強迫你用 stateful 的工作階段。因此,如果你希望伺服器考量在地化,必須要嘛:

  • 把它顯式當成工具參數傳入(inputSchema 中的 locale),要嘛
  • 從 ChatGPT 加到請求裡的 _meta["openai/locale"] 讀取。

以下是一個從 _meta 讀取 locale 的最簡 handler 範例:

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,或同時也放進工具 schema,稍後我們會獨立討論。

3. 模型一:單一多語 MCP 伺服器(「多語單體」)

先從最簡單的架構開始。你有一台 MCP 伺服器、一個 URL、一個部署、一份程式碼。在每個工具內你:

  1. 取得 locale(來自 _meta 或參數)。
  2. 根據 locale 選擇所需的資源:gift_catalog.en.jsongift_catalog.ru.json,等等。
  3. 以所需語言回傳結果。

GiftGenius 範例

假設我們有兩個目錄檔:

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

寫個小幫手 loadGiftCatalog(locale),用來選擇正確檔案:

async function loadGiftCatalog(locale: string) {
  const lang = locale.split("-")[0]; // "en-US" → "en"
  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 傳進去即可。 日期、貨幣與其他地區相關的格式也都可以同理處理。

這種模型的優缺點

為避免篇幅過長,先把第一種模型(單一 MCP)的優缺點整理成一張小表格(與 Gateway 的比較稍後會再回來看)。

指標 單一多語 MCP
MCP 執行個體數 1
在哪裡考量 locale 在各工具的程式碼內
部署與擴充 較簡單,單一據點
目錄在地化 以條件載入檔案/請求
出現 iflocale ...)的程式碼 會變很多
支援不同市場/API 所有異質整合都在同一套程式碼內

此模型適用於:

  • MVP 與小型應用,只有 2–3 種語言且市場差異不大;
  • 教學專案(例如本課程中的 GiftGenius)。

不太適合在下列情況:

  • 語言數量變多,
  • 不同市場的團隊與資料/整合差異極大(獨立資料庫、各自的電商 API 與法規要求)。

這正是第二種模型登場的時候。

4. 模型二:MCP Gateway + 單語後端伺服器

現在想像 GiftGenius 同時在美國、俄羅斯與德國運作。美國連 Amazon API,俄羅斯連 Ozon,德國連在地零售商。每個市場各有其契約、特性與團隊。把全部硬塞進單一 MCP 單體並不理想。

模型二的想法是:

在 ChatGPT 與實際 MCP 服務之間放一層 Gateway。對 ChatGPT 而言,這只是另一個 MCP 伺服器,但它會把請求路由到不同的後端伺服器;每個後端僅「說」一種語言並服務一個市場。

示意圖

先畫出兩種模型的對照。

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

在第二種模型裡,對 ChatGPT 來說可見的只有一個 MCP endpoint——Gateway。Gateway 會分析 _meta["openai/locale"] 與/或 _meta["openai/userLocation"],並選擇正確的後端。

Gateway 會做什麼(就本講範圍)

重點是不要把 Gateway 變成「帶有全部商業邏輯的第二個單體」。在我們這個章節中,它的角色很節制:

  1. 接收來自 ChatGPT 的 MCP 訊息(包含 _meta)。
  2. 取出 locale / userLocation。
  3. 依此選擇合適的後端伺服器。
  4. 把請求(JSON‑RPC)代理轉送過去,並把回應轉回來。

至於要用哪份禮物目錄、如何呼叫 Amazon 或 Ozon,這些決策仍留在各語言的 MCP 伺服器內。Gateway 並不知道「什麼是送給岳母的理想禮物」。它只需要知道 ru-RU 該去 mcp-giftgenius-ru,而 en-US 該去 mcp-giftgenius-en

最簡 MCP Gateway 骨架(TypeScript)

為了不陷入細節,我們大幅簡化。假設我們有個幫手 callDownstreamTool,能透過 JSON‑RPC 與內部 MCP 伺服器通訊(也可以是 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. 兩種模型對比

先前我們分別看了「單一 MCP」的優缺點。現在把兩種模型依主要面向並列比較。

指標 單一多語 MCP Gateway + 單語 MCP 伺服器
MCP 服務數 1 1 個 Gateway + N 個後端伺服器
在哪裡考量 locale 在每個工具內(if locale ... 的邏輯) 在 Gateway 進行路由;各服務內語言固定
UX 彈性(切換語言) 容易,皆在同一處,LLM 只需改 locale 可行,但需設計 Gateway 如何切換後端
基礎設施複雜度 最低 較高:每種語言需獨立部署
市場隔離度 低:同一份程式、同一個行程 高:RU 伺服器掛了不影響 EN,反之亦然
支援不同團隊 較難切分責任 自然:RU、EN、DE 團隊可各自開發其 MCP
在地化邏輯在程式碼中的位置 與商業邏輯混在各個 handler 裡 集中在 Gateway 與各後端服務的邊界

在本課程中,我們主要採用模型一(單一 MCP + locale 作為參數),而將 Gateway 視為當你有「真正的業務」且跨多市場時的自然擴充路徑。 既然 Gateway 是自然的下一步,我們就來看這個架構中的一個關鍵細節:如何把使用者的 locale 與國家放入工作階段狀態中。

6. 在 Gateway 內將 locale 作為用戶端狀態的一部分

到目前為止我們假設每個請求都帶齊所有資訊。但在真實世界,讓部分資訊存放在工作階段狀態會更方便。比如:

  • 使用者第一次帶著 locale = "ru-RU"userLocation.country = "RU" 來;
  • 之後你希望把他的所有請求都路由到 RU 後端,即便中間有些呼叫沒有在參數中顯式帶上 locale。

MCP 有個很實用的欄位 _meta["openai/subject"]——OpenAI 送到你服務的匿名使用者識別碼。可以把它當成工作階段的 key。

在記憶體中的簡單狀態實作

我們在 Gateway 中寫一個小小的 state 層(當然,正式環境裡應該用 Redis 或其他外部儲存,而不是 Map)。

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 的 handler 裡,可以先更新狀態,再用它來選擇後端伺服器:

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:兩個情境與架構選擇的影響

為了避免只是在談抽象方塊,我們看看 GiftGenius 的具體情境。

情境一:使用者在俄羅斯,使用俄語書寫

條件如下:

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

使用者訊息:「幫我挑一份送給同事的禮物,他喜歡桌遊,預算 3000 盧布以內」。

在模型一(單一 MCP):

  1. handler 從 _meta 讀到 "ru-RU" 的 locale。
  2. 載入 gift_catalog.ru.json,其中所有名稱為俄文、幣別為盧布。
  3. 依類別與預算過濾,回傳俄文的結構化禮物清單。

在模型二(Gateway + 單語服務):

  1. Gateway 讀取 locale 與 userLocation,判定為 RU 使用者。
  2. suggest_gifts 呼叫導向 mcp-giftgenius-ru
  3. 該服務只處理俄文目錄與 Ozon API,並以盧布回傳禮物。

兩種情況下使用者都會看到母語內容,但在第二種情況,你的英文 MCP 伺服器甚至完全不知道俄羅斯目錄的存在。

情境二:使用者在德國,使用英文書寫

條件如下:

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

使用者訊息:「Gift for my German coworker, budget 50 EUR」。

在模型一:

  • locale "en" 代表英文內容,
  • 而 country "DE" 可以用來選擇歐洲區的目錄(以歐元計價且更符合當地品項)。

在模型二:

  • Gateway 可以裁定:locale = "en" → 英文服務,但 country = "DE" → 用歐洲倉的商品;依你的商業邏輯,你可以:
  • 要嘛把請求送到 mcp-giftgenius-en,並附上 country=DE
  • 要嘛維運獨立的歐洲服務 mcp-giftgenius-eu

這裡可以清楚看見,語言(locale)與地區(userLocation)是不同維度;Gateway 是把兩者整合成「要叫哪個服務、要展示哪些商品」決策的好位置。

8. 在工具 schema 中放 locale,還是只用 _meta 的 locale?

無論你採用單一 MCP,還是 Gateway + 單語服務,最後都得面對一個重要但細膩的問題:把 locale 只放在 _meta,還是把它做成工具的參數?

做法有兩種。

第一種:只依賴 _meta

這樣 schema 不會被多出來的欄位弄得雜亂。伺服器從 extra._meta 讀到 locale 後自行決策。在模型一中,這往往已足夠。

第二種:locale(以及可能的 currency)明確加入工具的 inputSchema

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 路由到不同服務或資源時,特別好用。

實務上常會兩者並用:schema 中有 locale 欄位,但若模型沒有填,伺服器就回退到 _meta["openai/locale"]。

9. 在地化 vs Gateway「過度邏輯」的邊界在哪裡

很容易掉進一個陷阱:既然我們有個聰明的 Gateway,那就讓它:

  • 自己決定要展示哪些禮物,
  • 自己格式化日期與價格,
  • 自己彙整點擊報表,等等。

這聽起來很迷人,但會把 Gateway 變成「第二個單體」,並讓更新與維運更困難。在產業中的 API Gateway 實務(MCP Gateway 的角色本質上就是個 Gateway)會把重心放在幾件事:驗證、授權、路由與輕量級的上下文增補。例如,Gateway 可以把 HTTP 標頭轉成好用的中繼資料。商業邏輯與繁重的操作應該留在後端服務。

對在地化而言,這代表:

  • Gateway 可以剖析 _meta["openai/locale"] 與 _meta["openai/userLocation"]。
  • 可以把它們記在用戶端狀態中。
  • 可以選擇正確的語言伺服器,或替請求加上 locale/country 欄位。

但禮物挑選、依年齡或預算的過濾等——這些都應留在 MCP 後端服務內。

10. 透過 MCP 與 Gateway 設計在地化時的常見錯誤

錯誤一:只靠偵測使用者文字來「猜」語言。
有時會想把訊息文字丟進語言偵測器,然後據此決定要呼叫哪個服務。這可作為備援,但不應是主機制。平台已經提供 openai/localeopenai/userLocation,會考量 ChatGPT 設定與使用者環境。無視這些訊號、改玩「猜語言」,是讓 UX 在各種邊緣情境出錯的快捷方式。

錯誤二:只把 locale 放在模型腦海裡,而不傳到伺服器。
如果 locale 既不出現在 _meta,也不在工具參數中,伺服器就不知道使用者的語言。模型或許能把「книги」猜成 books,但這並不可靠,尤其在你有複雜分類時。正確做法是顯式傳遞 locale:或作為 locale 參數,或從 _meta 讀取,並圍繞它設計架構。

錯誤三:把所有在地化的商業邏輯搬進 Gateway。
若 Gateway 開始自己挑禮物、打資料庫、對接外部 API,它就不再是輕量路由器,而成了難以擴充與更新的重型服務。最後你會得到兩個單體。讓 Gateway 盡可能「笨」:看 locale/userLocation,選擇合適後端,並把中繼資料妥善傳遞即可。

錯誤四:路由只硬綁 IP 或 userLocation。
有時會想簡化成「如果國家是 RU 就去 RU 伺服器」。但使用者可能人在德國卻想要俄文介面,或在工作階段中突然說「switch to English」。如果 Gateway 不考量 openai/locale 與使用者可能想切換語言的需求,路由就會「硬梆梆」並破壞 UX。更好的做法是同時參考 locale 與 userLocation,並允許透過工作階段狀態覆寫。

錯誤五:不使用 _meta["openai/subject"],而在每個參數裡重複所有設定。
當你在每個工具參數裡都拖著 localecountrycurrencyuserId 等半個介面,日子會很難過。MCP 已經透過 _meta["openai/subject"] 傳遞匿名使用者 ID,你可以在 Gateway 或後端保存所有這些資訊。這能簡化契約,並降低參數不同步的風險。

錯誤六:沒有演進策略——一開始就要蓋一個支援十種語言的巨型 Gateway。
很多人一開始想做到完美:Gateway、五種語言、三個地區、十個 MCP 服務。實務上更簡單的路徑是:先用「單一 MCP + locale 參數或 _meta」把行為打穩,再隨著成長逐步抽出 Gateway 與單語服務。試圖一開始就建一座巨型「動物園」,幾乎保證會拖慢上線並讓偵錯變難。

1
問卷/小測驗
在地化,等級 9,課堂 4
未開放
在地化
在地化(UI、資料、函式描述)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION