CodeGym /Các khóa học /ChatGPT Apps /MCP Gateway và kiến trúc bản địa hóa: máy chủ đơn ngữ, lo...

MCP Gateway và kiến trúc bản địa hóa: máy chủ đơn ngữ, locale như tham số, trạng thái client

ChatGPT Apps
Mức độ , Bài học
Có sẵn

1. Tại sao cần nghĩ về kiến trúc bản địa hóa ngay từ đầu

Khi bạn chỉ có một ngôn ngữ và một catalog nhỏ, mọi thứ rất đơn giản: bạn giữ gift_catalog.json, toàn bộ văn bản bằng tiếng Nga, và MCP‑server thành thật trả về các món quà đó cho tất cả mọi người. Nhưng ngay khi bạn muốn:

  • UI tiếng Anh cho Hoa Kỳ và châu Âu,
  • catalog riêng tiếng Nga với búp bê Matryoshka và sách tiếng Nga,
  • các thị trường khác nhau (Amazon cho Hoa Kỳ, Ozon cho Liên bang Nga),

cách tiếp cận ngây thơ “thêm một if (locale === "ru") trong mỗi handler” bắt đầu biến mã thành “cây thông Noel”.

MCP vừa là một giao thức, vừa là một triển khai máy chủ cho giao thức đó. Server nhận yêu cầu từ ChatGPT kèm metadata, bao gồm cả localeuserLocation. Câu hỏi không phải “nó có đọc được locale không”, mà là bạn tính đến tín hiệu này ở đâu trong kiến trúc. Có thể làm ở mỗi công cụ, hoặc trích một phần logic sang một lớp riêng — Gateway.

Một kiến trúc bản địa hóa tốt phải trả lời ba câu hỏi:

  1. Chúng ta quyết định dùng ngôn ngữ và vùng nào ở đâu.
  2. Chúng ta chọn dữ liệu và tích hợp (catalog, API cửa hàng, tiền tệ) ở đâu.
  3. Chúng ta lưu trạng thái người dùng (locale, tiền tệ, có thể cả sở thích) ở đâu và như thế nào để không phải truyền tay mỗi lần.

Hôm nay chúng ta sẽ đi qua những điều đó.

2. MCP, _meta và tính chất stateless: vì sao cần truyền locale một cách tường minh

Trước khi quyết định nơi nào trong kiến trúc sẽ xét locale, hữu ích là nên nhớ một MCP‑request trông như thế nào ở cấp độ giao thức và nền tảng đã truyền những metadata gì.

Một sự thật quan trọng: các yêu cầu MCP là thông điệp JSON‑RPC. Mỗi thông điệp là độc lập; giao thức không ép bạn dùng phiên stateful. Do đó, nếu bạn muốn server xét đến locale, bạn phải hoặc:

  • truyền tường minh như một tham số của công cụ (locale trong inputSchema), hoặc
  • đọc từ _meta["openai/locale"] do ChatGPT thêm vào yêu cầu.

Ví dụ đơn giản về handler đọc locale từ _meta:

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;

    // Sau đó dùng locale và country để chọn catalog
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

Ở đây chúng ta không truyền locale qua tham số, mà dựa vào _meta đã được SDK đặt vào extra. Đây là phương án hoàn toàn ổn và sẽ hữu ích cho mô hình đầu tiên — một MCP đa ngôn ngữ.

Ở mô hình thứ hai — với Gateway — _meta cũng đóng vai trò then chốt: gateway đọc locale từ metadata và dựa vào đó quyết định sẽ gửi yêu cầu đi đâu tiếp theo. Cách lưu locale ở dạng nào — chỉ trong _meta hay thêm vào schema của công cụ — chúng ta sẽ bàn riêng ở phần dưới.

3. Mô hình 1: một MCP đa ngôn ngữ (“polyglot‑monolith”)

Bắt đầu từ phương án kiến trúc đơn giản nhất. Bạn có một MCP‑server, một URL, một lần triển khai, một codebase. Bên trong mỗi công cụ, bạn sẽ:

  1. Lấy locale (từ _meta hoặc từ tham số).
  2. Dựa vào locale chọn các tài nguyên phù hợp: gift_catalog.en.json, gift_catalog.ru.json, v.v.
  3. Trả về kết quả bằng ngôn ngữ cần thiết.

Ví dụ cho GiftGenius

Giả sử chúng ta có hai tệp catalog:

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

Tạo một helper nhỏ loadGiftCatalog(locale) để chọn tệp cần dùng:

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; // mảng quà tặng
}

Giờ công cụ suggest_gifts có thể chỉ việc gọi helper này:

server.registerTool(
  "suggest_gifts",
  { title: "Gợi ý quà tặng", 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 } };
  }
);

Như vậy, phần bản địa hóa được giấu ở một nơi — trong loadGiftCatalog, còn các công cụ chỉ việc truyền locale vào đó. Tương tự, bạn có thể chọn định dạng ngày, tiền tệ và các yếu tố phụ thuộc vùng khác.

Ưu và nhược điểm của mô hình này

Để gọn nhẹ, ta tóm tắt ưu/nhược của mô hình đầu tiên này trong một bảng nhỏ (mới chỉ nói về “một MCP” — so sánh với Gateway sẽ quay lại sau).

Tiêu chí Một MCP đa ngôn ngữ
Số lượng MCP‑instance 1
Nơi xét tới locale Trong mã của các công cụ
Triển khai và mở rộng Đơn giản, một điểm
Bản địa hóa catalog Qua việc nạp có điều kiện tệp/yêu cầu
if (locale ...) Ngày càng nhiều
Hỗ trợ các thị trường/API khác nhau Tất cả “zoo” nằm trong cùng một codebase

Mô hình này phù hợp cho:

  • MVP và ứng dụng nhỏ, nơi chỉ có 2–3 ngôn ngữ và thị trường không quá khác nhau;
  • dự án học tập (ví dụ GiftGenius của chúng ta trong khóa học).

Và kém phù hợp khi:

  • số lượng ngôn ngữ tăng nhiều,
  • đội ngũ và dữ liệu cho các thị trường khác nhau rất khác nhau (DB riêng, e‑commerce API riêng, yêu cầu pháp lý riêng).

Chính trong các trường hợp đó, mô hình thứ hai xuất hiện.

4. Mô hình 2: MCP Gateway + các máy chủ backend đơn ngữ

Giờ tưởng tượng GiftGenius hoạt động ở Mỹ, ở Nga, và giả sử cả ở Đức. Với Mỹ bạn gọi Amazon API, với Nga — Ozon, với Đức — nhà bán lẻ nội địa. Mỗi thị trường có hợp đồng, đặc thù và đội ngũ riêng. Nhét tất cả vào một MCP‑monolith thì không dễ chịu.

Ý tưởng của mô hình 2 là:

Đặt Gateway giữa ChatGPT và các dịch vụ MCP thực. Với ChatGPT, nó chỉ là một MCP‑server khác; bên trong nó định tuyến yêu cầu tới các máy chủ backend khác nhau, mỗi máy chủ “nói” một ngôn ngữ và phục vụ một thị trường duy nhất.

Trông như thế nào trên sơ đồ

Trước hết, vẽ so sánh hai mô hình.

flowchart LR
    subgraph Model1["Mô hình 1: Một MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (đa ngôn ngữ)"]
    end

    subgraph Model2["Mô hình 2: Gateway + đơn ngữ"]
      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, Cửa hàng địa phương)"]
    end

Trong mắt ChatGPT, ở mô hình thứ hai chỉ có một MCP‑endpoint — Gateway. Bên trong, nó phân tích _meta["openai/locale"] và/hoặc _meta["openai/userLocation"] và chọn backend phù hợp.

Gateway làm gì (trong phạm vi bài này)

Quan trọng là đừng biến Gateway thành “monolith thứ hai với toàn bộ business logic”. Trong mô-đun này vai trò của nó rất giới hạn:

  1. Nhận thông điệp MCP từ ChatGPT (bao gồm _meta).
  2. Trích locale / userLocation.
  3. Dựa vào đó chọn máy chủ backend cần thiết.
  4. Proxy yêu cầu (JSON‑RPC) tới đó và trả kết quả về.

Mọi quyết định như chọn catalog quà tặng nào, gọi Amazon hay Ozon thế nào, đều nằm trong từng MCP‑server theo ngôn ngữ. Gateway không biết “món quà hoàn hảo cho mẹ vợ” là gì. Nó chỉ cần biết rằng với ru-RU thì đi mcp-giftgenius-ru, còn với en-USmcp-giftgenius-en.

Bộ khung MCP Gateway đơn giản bằng TypeScript

Đơn giản hóa mạnh để tránh sa đà chi tiết. Giả sử ta có helper callDownstreamTool có thể nói chuyện với các MCP‑server nội bộ qua JSON‑RPC (có thể là HTTP hoặc kết nối SSE lâu dài, nhưng chi tiết để dành cho mô-đun 16).

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

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

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // mặc định
  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"
    // Gọi cùng công cụ trên backend phù hợp
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

Các MCP‑server nội bộ đăng ký suggest_gifts với đúng cùng một hợp đồng, nhưng mỗi server chỉ làm việc với ngôn ngữ/thị trường của mình và không biết rằng ở đâu đó còn có các ngôn ngữ khác.

Tương tự, Gateway có thể proxy listTools, listResources và các phương thức MCP khác, nhưng đó là chủ đề của mô-đun khác.

5. So sánh hai mô hình cho bản địa hóa

Trước đó ta đã xem riêng ưu/nhược của mô hình “một MCP”. Giờ tổng hợp sự khác nhau của cả hai mô hình theo các tham số chính.

Tiêu chí Một MCP đa ngôn ngữ Gateway + các MCP‑server đơn ngữ
Số lượng dịch vụ MCP 1 1 Gateway + N backend‑server
Nơi xét tới locale Bên trong từng công cụ (logic if locale ...) Tại Gateway (định tuyến); bên trong dịch vụ, ngôn ngữ là cố định
Tính linh hoạt UX (đổi ngôn ngữ) Dễ, tất cả ở một nơi, LLM chỉ việc đổi locale Có thể, nhưng cần thiết kế cách Gateway chuyển backend
Độ phức tạp hạ tầng Tối thiểu Cao hơn: cần deploy riêng cho từng ngôn ngữ
Cô lập theo thị trường Thấp: một mã, một tiến trình Cao: RU rơi không làm EN hỏng và ngược lại
Hỗ trợ các đội khác nhau Khó tách trách nhiệm Tự nhiên: đội RU, EN, DE có thể phát triển MCP riêng
Logic bản địa hóa trong mã Trộn với business logic ở mỗi handler Tập trung ở Gateway và trong ranh giới backend cụ thể

Trong khóa học này, chúng ta chủ yếu theo mô hình 1 (một MCP + locale như tham số), còn mô hình với Gateway sẽ được xem như con đường mở rộng tự nhiên khi bạn đã có “bài toán kinh doanh thật” với hàng chục thị trường. Tuy vậy, vì Gateway là bước tiếp theo tự nhiên, ta sẽ xem một chi tiết quan trọng của kiến trúc này: lưu locale và quốc gia của người dùng trong trạng thái phiên như thế nào.

6. Locale như một phần trạng thái client trong Gateway

Cho đến giờ, ta giả định mỗi yêu cầu đều chứa đủ thứ cần thiết. Nhưng trong thực tế, tiện hơn là giữ một phần thông tin trong trạng thái phiên. Ví dụ:

  • người dùng vào một lần với locale = "ru-RU"userLocation.country = "RU";
  • sau đó bạn muốn định tuyến mọi yêu cầu của họ sang RU‑backend, ngay cả khi một số lần gọi trung gian không có locale rõ ràng trong tham số.

MCP có trường hữu ích _meta["openai/subject"] — định danh ẩn danh của người dùng do OpenAI gửi tới dịch vụ của bạn. Có thể dùng nó như khóa phiên.

Triển khai trạng thái đơn giản trong bộ nhớ

Viết một lớp state siêu nhỏ trong Gateway (tất nhiên, production thì thay Map bằng Redis hay kho ngoài khác).

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

Giờ trong handler của Gateway ta có thể cập nhật trạng thái trước, rồi dùng nó khi chọn backend:

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

Như vậy, bạn “ghi nhớ” một lần ánh xạ clientIdlocale, country và có thể dùng nó trong mọi lần gọi công cụ sau đó, không cần copy các trường vào từng tham số.

Tương tự, Gateway có thể ghi nhớ tiền tệ ưa thích, định dạng giá hoặc các cài đặt khác hữu ích cho logic thương mại (nhưng phần này nhiều hơn trong mô-đun về ACP).

7. GiftGenius: hai kịch bản và tác động của lựa chọn kiến trúc

Để tránh cảm giác nói chuyện trừu tượng, hãy xét các kịch bản cụ thể của GiftGenius.

Kịch bản 1: Người dùng từ Nga, viết bằng tiếng Nga

Giả sử:

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

Người dùng viết: “Hãy chọn quà cho đồng nghiệp, thích board game, tối đa 3000 rúp.”

Trong mô hình 1 (một MCP):

  1. Handler đọc locale từ _meta, nhận "ru-RU".
  2. Nạp gift_catalog.ru.json, nơi mọi tên đều bằng tiếng Nga, giá bằng rúp.
  3. Lọc theo danh mục và ngân sách, trả danh sách quà tặng có cấu trúc bằng tiếng Nga.

Trong mô hình 2 (Gateway + đơn ngữ):

  1. Gateway đọc locale và userLocation, quyết định đây là người dùng RU.
  2. Chuyển lời gọi suggest_gifts sang mcp-giftgenius-ru.
  3. Server đó chỉ làm việc với catalog tiếng Nga và Ozon API, trả quà bằng rúp.

Cả hai trường hợp người dùng đều thấy nội dung bằng ngôn ngữ của họ, nhưng ở phương án thứ hai, MCP tiếng Anh của bạn thậm chí không biết đến catalog dành cho Nga.

Kịch bản 2: Người dùng từ Đức, viết bằng tiếng Anh

Giờ thì:

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

Người dùng viết: “Gift for my German coworker, budget 50 EUR”.

Trong mô hình 1:

  • locale "en" cho văn bản tiếng Anh,
  • còn country "DE" có thể dùng để chọn catalog nơi giá bằng euro và hàng hóa phù hợp châu Âu.

Trong mô hình 2:

  • Gateway có thể quyết định rằng locale = "en" → dịch vụ tiếng Anh, nhưng country = "DE" → hàng từ kho châu Âu; tùy business logic bạn có thể:
  • hoặc chuyển yêu cầu tới mcp-giftgenius-en với tham số country=DE,
  • hoặc có một mcp-giftgenius-eu riêng cho châu Âu.

Ở đây thấy rõ ngôn ngữ (locale) và vùng (userLocation) là hai chiều khác nhau, và Gateway là nơi thuận tiện để “ghép” chúng thành quyết định “gọi dịch vụ nào và hiển thị sản phẩm gì”.

8. Locale trong schema của công cụ hay chỉ để trong _meta

Bất kể bạn dùng một MCP hay cặp Gateway + dịch vụ đơn ngữ, phần cuối này đáng bàn: lưu locale chỉ trong _meta hay đưa nó thành tham số công cụ?

Có hai cách tiếp cận.

Thứ nhất: dựa hoàn toàn vào _meta.

Cách này tiện ở chỗ schema công cụ không bị “rác” thêm một trường. Server đọc locale từ extra._meta và tự quyết định. Trong mô hình 1, thường là đủ.

Thứ hai: thêm tường minh locale (và có thể currency) vào inputSchema của công cụ.

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

Sau đó, trong system‑prompt bạn có thể yêu cầu model luôn điền locale vào tham số, dùng giá trị từ ngữ cảnh người dùng. Điều này làm ý định trở nên minh bạch: ngay trong JSON arguments có thể thấy server phải làm việc với ngôn ngữ nào. Cách này đặc biệt hữu ích trong kiến trúc phức tạp hơn, nơi có một MCP chung, và bên trong định tuyến theo locale tới các dịch vụ hay tài nguyên khác nhau.

Trong thực tế, thường kết hợp cả hai: schema có trường locale, nhưng nếu vì lý do nào đó model không điền, server sẽ dự phòng bằng _meta["openai/locale"].

9. Ranh giới giữa bản địa hóa và “logic thừa” trong Gateway nằm ở đâu

Cái bẫy dễ rơi vào: vì đã có Gateway “thông minh”, hãy để nó:

  • tự quyết định sẽ hiển thị quà gì,
  • tự định dạng ngày và giá,
  • tự gom báo cáo click, v.v.

Nghe hấp dẫn, nhưng sẽ biến Gateway thành “monolith thứ hai” và làm phức tạp việc cập nhật/vận hành. Trong thực tiễn về API gateway (và MCP Gateway có cùng vai trò), người ta tập trung vào vài nhiệm vụ: xác thực, phân quyền, định tuyến và làm giàu ngữ cảnh nhẹ nhàng. Ví dụ, gateway có thể chuyển đổi HTTP headers thành metadata tiện dùng. Business logic và tác vụ nặng phải sống ở backend‑service.

Với bản địa hóa, điều đó có nghĩa là:

  • Gateway có thể parse _meta["openai/locale"] và _meta["openai/userLocation"].
  • Có thể ghi nhớ chúng trong trạng thái client.
  • Có thể chọn server theo ngôn ngữ phù hợp hoặc thêm trường locale/country vào yêu cầu.

Nhưng việc chọn quà, lọc theo độ tuổi, ngân sách, v.v. — tất cả nên ở các MCP‑backend.

10. Các lỗi thường gặp khi thiết kế bản địa hóa qua MCP và Gateway

Lỗi số 1: Chỉ dựa vào “đoán” ngôn ngữ từ văn bản người dùng.
Đôi khi muốn lấy văn bản, chạy qua language‑detector và dựa vào đó quyết định gọi server nào. Nó có thể hữu ích như phương án dự phòng, nhưng không nên là cơ chế chính. Nền tảng đã cho bạn openai/localeopenai/userLocation, vốn xét đến thiết lập ChatGPT và môi trường người dùng. Bỏ qua các tín hiệu này để “đoán ngôn ngữ” là cách hiệu quả để phá UX trong những tình huống khó lường.

Lỗi số 2: Chỉ giữ locale “trong đầu” của model và không truyền nó tới server.
Nếu locale không xuất hiện trong _meta hay tham số công cụ, server sẽ không biết ngôn ngữ của người dùng. Model có thể cố dịch “книги” thành books, nhưng điều đó không đáng tin, nhất là khi bạn có các danh mục phức tạp. Cách đúng là truyền tường minh locale: hoặc qua tham số locale, hoặc đọc từ _meta và xây kiến trúc xoay quanh nó.

Lỗi số 3: Mang toàn bộ business logic bản địa hóa vào Gateway.
Nếu Gateway bắt đầu tự chọn quà, truy vấn database và làm việc với API ngoài, nó không còn là bộ định tuyến nhẹ mà thành dịch vụ nặng, khó mở rộng/cập nhật. Kết quả là bạn có hai monolith thay vì một. Hãy giữ Gateway càng “ngốc” càng tốt: nhìn vào locale/userLocation, chọn backend phù hợp và chuyển metadata một cách gọn gàng.

Lỗi số 4: Ràng buộc định tuyến cứng chỉ theo IP hoặc userLocation.
Đôi khi muốn làm đơn giản: “nếu country là RU — đi RU‑server”. Nhưng người dùng có thể ở Đức và vẫn muốn giao diện tiếng Nga, hoặc có thể nói “switch to English” giữa phiên. Nếu Gateway không xét openai/locale và khả năng người dùng đổi ngôn ngữ, định tuyến sẽ “đông cứng” và phá UX. Tốt hơn là dựa vào tổ hợp locale và userLocation, đồng thời cho phép ghi đè qua trạng thái phiên.

Lỗi số 5: Không dùng _meta["openai/subject"] và phải lặp mọi tham số trong từng lời gọi.
Khi bạn phải kéo theo locale, country, currency, userId và nửa tá tham số khác trong mỗi tham số công cụ, cuộc sống sẽ nhanh chóng mệt mỏi. MCP đã truyền định danh ẩn danh qua _meta["openai/subject"], và bạn có thể lưu toàn bộ thông tin này trong trạng thái client ở Gateway hay backend‑server. Điều đó giúp hợp đồng gọn hơn và giảm rủi ro lệch tham số.

Lỗi số 6: Thiếu chiến lược tiến hóa: “xây Gateway phức tạp cho mười ngôn ngữ ngay từ đầu”.
Rất dễ muốn làm hoàn hảo ngay: Gateway, năm ngôn ngữ, ba vùng, mười MCP‑service. Trên thực tế, đơn giản hơn là bắt đầu với mô hình “một MCP + tham số hoặc _meta về locale”, làm cho hành vi ổn định, rồi tách Gateway và dịch vụ đơn ngữ theo đà tăng trưởng. Cố gắng dựng ngay một “sở thú” khổng lồ gần như chắc chắn sẽ kéo dài phát hành và làm khó gỡ lỗi.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Bản địa hóa
Bản địa hóa (UI, dữ liệu, mô tả chức năng)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION