JavaRush /Курси /ChatGPT Apps /MCP Gateway і архітектура локалізації: одномовні сервери,...

MCP Gateway і архітектура локалізації: одномовні сервери, locale як параметр, стан клієнта

ChatGPT Apps
Рівень 9 , Лекція 4
Відкрита

1. Навіщо взагалі думати про архітектуру локалізації

Поки у вас одна мова й невеликий каталог, усе просто: ви зберігаєте gift_catalog.json, усі тексти — однією мовою, а MCP‑сервер чесно видає ці подарунки всім підряд. Але щойно ви хочете:

  • англомовний UI для США та Європи,
  • окремий україномовний каталог із, скажімо, вишиванками та книжками українською,
  • різні ринки (Amazon для США, Rozetka для України),

наївний підхід «у кожному обробнику ще один if (locale === "uk")» швидко перетворює код на різдвяну ялинку.

MCP — це, з одного боку, протокол, а з іншого — серверна реалізація цього протоколу. Сервер отримує запити від ChatGPT разом із метаданими, зокрема locale і userLocation. Питання не в тому, «чи вміє він прочитати locale». Питання в іншому: де саме в архітектурі ви враховуєте цей сигнал. Це можна робити в кожному інструменті. А можна винести частину логіки в окремий шар — Gateway.

Добра архітектура локалізації має відповідати на три запитання:

  1. Де ми ухвалюємо рішення, яку мову й регіон використовувати.
  2. Де ми обираємо потрібні дані й інтеграції (каталоги, API магазинів, валюти).
  3. Де і як ми зберігаємо стан користувача (locale, валюта, можливо, якісь уподобання), щоб не передавати це вручну щоразу.

Сьогодні ми саме це й розберемо.

2. MCP, _meta і stateless‑природа: чому locale потрібно передавати явно

Перш ніж вирішувати, де саме в архітектурі враховувати locale, корисно згадати, як виглядає MCP‑запит на рівні протоколу. А також — які метадані платформа вже передає.

Нагадаю важливий факт: MCP‑запити — це JSON‑RPC‑повідомлення. Кожне повідомлення є самодостатнім, а протокол не навʼязує вам сесію зі станом. Тому, якщо ви хочете, щоб сервер враховував locale, його потрібно або:

  • передавати явно як аргумент інструмента (locale у inputSchema), або
  • читати з _meta["openai/locale"], яке ChatGPT додає до запиту.

Найпростіший приклад обробника, який читає locale з _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;

    // Далі використовуємо locale і country для вибору каталогу
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

Тут ми не передаємо locale через аргументи, а покладаємося на _meta, який SDK уже поклав у extra. Це цілком робочий варіант — і він нам знадобиться в першій моделі, з одним багатомовним MCP.

У другій моделі — із Gateway — _meta теж відіграє ключову роль. Шлюз читає locale з метаданих і на цій основі вирішує, куди далі надіслати запит. Про те, у якому саме вигляді зберігати locale — лише в _meta чи ще й у схемах інструментів, — поговоримо окремим блоком нижче.

3. Модель 1: один багатомовний MCP‑сервер («поліглот‑моноліт»)

Почнімо з найпростішого архітектурного варіанта. У вас є один MCP‑сервер, один URL, одне розгортання, одна кодова база. Усередині кожного інструмента ви:

  1. Отримуєте locale (з _meta або з аргументу).
  2. На основі locale обираєте потрібні ресурси: gift_catalog.en.json, gift_catalog.uk.json тощо.
  3. Повертаєте результат уже потрібною мовою.

Приклад для GiftGenius

Припустімо, у нас є два файли з каталогами:

  • data/gift_catalog.en.json
  • data/gift_catalog.uk.json

Зробімо невеликий допоміжний метод loadGiftCatalog(locale), який обирає потрібний файл:

async function loadGiftCatalog(locale: string) {
  const lang = locale.split("-")[0]; // "en-US" → "en"
  const fileName = lang === "uk" ? "gift_catalog.uk.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 У коді інструментів
Розгортання та масштабування Простіше: одна точка
Локалізація каталогів Через умовне завантаження файлів/запитів
Коду if (locale ...) Стає багато
Підтримка різних ринків/API Увесь «зоопарк» в одному коді

Ця модель чудово підходить для:

  • MVP і невеликих застосунків, де 2–3 мови й не надто відмінні ринки;
  • навчальних проєктів (наприклад, наш GiftGenius у межах курсу).

Вона гірше підходить, коли:

  • мов стає багато,
  • команди й дані для різних ринків принципово різні (окремі БД, свої e‑commerce API, свої юридичні вимоги).

І саме в таких випадках на сцену виходить друга модель.

4. Модель 2: MCP Gateway + одномовні backend‑сервери

Тепер уявімо, що GiftGenius працює і в США, і в Україні, і, скажімо, у Німеччині. Для США ви звертаєтеся до Amazon API, для України — до Rozetka, для Німеччини — до локального ритейлера. У кожного ринку — свій контракт, свої особливості, своя команда. Тримати все це в одному MCP‑моноліті незручно.

Ідея моделі 2 така:

Між ChatGPT і реальними MCP‑сервісами стоїть Gateway. Для ChatGPT це просто ще один MCP‑сервер, а всередині він маршрутизує запити до різних backend‑серверів. Кожен із них «говорить» лише однією мовою і працює з одним ринком.

Як це виглядає на діаграмі

Спочатку намалюємо порівняння двох моделей.

flowchart LR
    subgraph Model1["Модель 1: Один MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (багатомовний)"]
    end

    subgraph Model2["Модель 2: Gateway + одномовні"]
      A2[ChatGPT] --> G[MCP Gateway]
      G --> U["GiftGenius MCP UA (uk-UA, Rozetka)"]
      G --> E["GiftGenius MCP EN (en-US, Amazon"]
      G --> D["GiftGenius MCP DE (de-DE, Local shop)"]
    end

З погляду ChatGPT у другій моделі є лише один MCP‑endpoint — Gateway. Усередині він аналізує _meta["openai/locale"] і/або _meta["openai/userLocation"] і обирає правильний backend.

Що робить Gateway (у контексті цієї лекції)

Важливо не перетворити Gateway на «другий моноліт» з усією бізнес‑логікою. У нашому модулі його роль доволі обмежена:

  1. Прийняти MCP‑повідомлення від ChatGPT (зокрема з _meta).
  2. Витягнути locale / userLocation.
  3. На цій основі обрати потрібний backend‑сервер.
  4. Проксіювати туди запит (JSON‑RPC) і повернути відповідь назад.

Усі рішення — який саме каталог подарунків брати і як саме звертатися до Amazon або Rozetka — лишаються всередині конкретного мовного MCP‑сервера. Gateway не знає, як виглядає «ідеальний подарунок для колеги». Йому достатньо знати, що для uk-UA треба йти в mcp-giftgenius-uk, а для en-US — у mcp-giftgenius-en.

Найпростіший скелет MCP Gateway на TypeScript

Сильно спростімо, щоб не потонути в деталях. Уявімо, що в нас є допоміжний метод callDownstreamTool, який уміє спілкуватися з внутрішніми MCP‑серверами через JSON‑RPC (це могли б бути 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];     // uk-UA → uk
  return ["uk", "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); // "uk" | "en" | "de"
    // Викликаємо той самий інструмент на потрібному backend-сервері
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

Внутрішні MCP‑сервери реєструють у себе suggest_gifts із точно таким самим контрактом, але кожен працює лише зі своєю мовою/ринком і не знає, що десь існують інші мови.

Так само Gateway може проксіювати listTools, listResources та інші MCP‑методи. Але це вже тема окремого модуля.

5. Порівняння двох моделей для локалізації

Раніше ми окремо подивилися на переваги й недоліки моделі «один MCP». Тепер зведемо відмінності обох моделей за основними параметрами.

Критерій Один багатомовний MCP Gateway + одномовні MCP‑сервери
Кількість MCP‑сервісів 1 1 Gateway + N backend‑серверів
Де враховується locale Усередині кожного інструмента (логіка if locale ...) У Gateway, який маршрутизує; усередині сервісів мова фіксована
UX‑гнучкість (зміна мови) Легко: усе в одному місці, LLM просто змінює locale Можливо, але потрібно продумати, як Gateway перемикатиме backend
Складність інфраструктури Мінімальна Вища: потрібні окремі розгортання для кожної мови
Ізоляція за ринками Низька: один код, один процес Висока: падіння UK‑сервера не ламає EN — і навпаки
Підтримка різних команд Складніше розділити відповідальність Природно: команди UK, EN, DE можуть розробляти свої MCP окремо
Логіка локалізації в коді Змішана з бізнес‑логікою в кожному обробнику Сконцентрована в Gateway та в межах конкретного backend‑сервісу

Для нашого навчального курсу ми здебільшого дотримуватимемося моделі 1 (один MCP + locale як параметр), а модель із Gateway розглядатимемо як природний шлях масштабування, коли у вас уже «справжній бізнес» із десятками ринків. Водночас, оскільки Gateway — логічний наступний крок, ми розглянемо одну важливу деталь цієї архітектури: як зберігати locale і країну користувача в стані сесії.

6. Locale як частина стану клієнта у Gateway

Досі ми припускали, що кожен запит містить усе необхідне. Але в реальному житті зручно тримати частину інформації в стані сесії. Наприклад:

  • користувач один раз прийшов із locale = "uk-UA" і userLocation.country = "UA";
  • далі ви хочете маршрутизувати всі його запити на uk‑backend, навіть якщо якісь проміжні виклики приходять без явно заданого locale в аргументах.

У 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 можна спочатку оновити стан, а потім використовувати його під час вибору 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);
  }
);

Таким чином, ви один раз «запамʼятовуєте» звʼязку clientIdlocale, country і далі можете використовувати її в усіх наступних викликах інструментів, не копіюючи ці поля в кожен аргумент.

Так само Gateway може запамʼятовувати бажану валюту, формат цін або інші налаштування, що знадобляться commerce‑логіці (але про це — уже докладніше в модулі про ACP).

7. GiftGenius: два сценарії та вплив вибору архітектури

Щоб не було відчуття, що ми просто обговорюємо абстрактні квадратики, подивімося на конкретні сценарії GiftGenius.

Сценарій 1: Користувач з України, пише українською

Нехай у нас:

  • _meta["openai/locale"] = "uk-UA",
  • _meta["openai/userLocation"].country = "UA".

Користувач пише: «Підберіть подарунок колезі: любить настільні ігри, до 3 000 ₴».

У моделі 1 (один MCP):

  1. Обробник читає locale з _meta і отримує "uk-UA".
  2. Завантажує gift_catalog.uk.json, де всі назви українською, а ціни — у гривнях.
  3. Фільтрує за категорією та бюджетом і повертає структурований список подарунків українською.

У моделі 2 (Gateway + одномовні сервери):

  1. Gateway читає locale і userLocation та вирішує, що це користувач з України.
  2. Спрямовує виклик suggest_gifts на mcp-giftgenius-uk.
  3. Той працює лише з українським каталогом і Rozetka 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 для Європи.

Тут добре видно, що locale (мова) та регіон (userLocation) — різні виміри. А Gateway — зручне місце, щоб поєднати їх у рішенні «який сервіс викликати і які товари показувати».

8. Locale у схемах інструментів vs locale лише в _meta

Незалежно від того, чи використовуєте ви один MCP або звʼязку Gateway + одномовні сервіси, насамкінець варто обговорити тонкий, але важливий момент: зберігати locale лише в _meta чи зробити його аргументом інструмента?

Є два підходи.

Перший: покладатися лише на _meta.

Це зручно тим, що схеми інструментів не захаращуються ще одним полем. Сервер читає locale з extra._meta і сам ухвалює рішення. У моделі 1 цього часто достатньо.

Другий: явно додати locale (і, можливо, currency) до inputSchema інструмента.

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

Далі в system‑prompt ви можете попросити модель завжди заповнювати locale аргументом, використовуючи значення з контексту користувача. Це робить наміри прозорими: у JSON‑аргументах прямо видно, якою мовою сервер має працювати. Такий підхід особливо корисний у складнішій архітектурі, де є один спільний MCP, який за locale усередині маршрутизує до різних сервісів або ресурсів.

На практиці часто поєднують обидва підходи: у схемах є поле locale, але якщо з якоїсь причини модель його не заповнила, сервер підстраховується через _meta["openai/locale"].

9. Де проходить межа між локалізацією та «зайвою логікою» в Gateway

Пастка, у яку легко скотитися: раз у нас є розумний Gateway, давайте він буде:

  • сам вирішувати, які подарунки показувати,
  • сам форматувати дати й ціни,
  • сам збирати звіти з кліків тощо.

Це звучить привабливо, але перетворює Gateway на «другий моноліт» і ускладнює його оновлення та експлуатацію. В індустріальних практиках API‑шлюзів (а MCP Gateway за роллю — той самий шлюз) фокус тримають на кількох завданнях: автентифікація, авторизація, маршрутизація та легке збагачення контексту. Наприклад, шлюз може перетворювати HTTP‑заголовки на зручні метадані. Бізнес‑логіка й «важкі» операції мають жити в backend‑сервісах.

Для локалізації це означає:

  • Gateway може розпарсити _meta["openai/locale"] і _meta["openai/userLocation"].
  • Може запамʼятати їх у стані клієнта.
  • Може обрати потрібний мовний сервер або додати до запиту поле locale/country.

Але сам підбір подарунків, фільтрація за віком, бюджетом та інше — усе це має лишатися в MCP‑backend‑ах.

10. Типові помилки під час проєктування локалізації через MCP і Gateway

Помилка №1: Покладатися лише на «вгадування» мови за текстом користувача.
Іноді хочеться взяти текст повідомлення, прогнати його через language‑detector і на цій підставі вирішувати, який сервер викликати. Це може бути корисним запасним варіантом, але не основним механізмом. Платформа вже дає вам openai/locale і openai/userLocation, які враховують налаштування ChatGPT та оточення користувача. Ігнорувати ці сигнали й грати в «вгадай мову» — надійний спосіб зламати UX у найнесподіваніших випадках.

Помилка №2: Зберігати locale лише «в голові моделі» і не передавати його на сервер.
Якщо locale ніде не фігурує ані в _meta, ані в аргументах інструмента, сервер нічого не знає про мову користувача. Модель, звісно, може спробувати перекласти рядок «книги» на books, але це ненадійно, особливо якщо у вас складні категорії. Правильний шлях — явно передавати locale: або через аргумент locale, або читати з _meta і будувати архітектуру навколо цього.

Помилка №3: Переносити всю бізнес‑логіку локалізації в Gateway.
Якщо Gateway починає сам обирати подарунки, ходити в бази даних і «воювати» із зовнішніми API, він перестає бути легким маршрутизатором і стає важким сервісом, який складно масштабувати й оновлювати. У результаті ви отримуєте два моноліти замість одного. Краще тримати Gateway максимально «простим»: він дивиться на locale/userLocation, обирає потрібний backend і акуратно передає метадані далі.

Помилка №4: Жорстка привʼязка маршрутизації лише до IP або userLocation.
Іноді хочеться зробити просто: «якщо країна UA — ідемо на UK‑сервер». Але користувач може перебувати в Німеччині й усе одно хотіти інтерфейс українською. Або він може попросити «switch to English» посеред сесії. Якщо ви в Gateway не враховуєте openai/locale і можливе бажання користувача змінити мову, маршрутизація стає «бетонною» й ламає UX. Краще спиратися на комбінацію locale і userLocation. Також варто зберігати можливість перевизначати налаштування через стан сесії.

Помилка №5: Не використовувати _meta["openai/subject"] і дублювати всі параметри в кожному аргументі.
Коли ви в кожному наборі аргументів інструмента починаєте тягнути locale, country, currency, userId і ще пів інтерфейсу, життя швидко стає сумним. MCP уже передає анонімний ідентифікатор користувача через _meta["openai/subject"], і ви можете зберігати всю цю інформацію в стані клієнта на боці Gateway або backend‑сервера. Це спростить контракти й зменшить ризик розсинхронізації аргументів.

Помилка №6: Відсутність стратегії еволюції: «одразу будуємо складний Gateway на десять мов».
Часто хочеться відразу зробити «ідеально»: Gateway, пʼять мов, три регіони, десять MCP‑сервісів. На практиці простіше почати з моделі «один MCP + locale‑параметр або _meta», довести поведінку до стабільної, а потім виділяти Gateway і одномовні сервіси в міру зростання. Спроба одразу побудувати величезний «зоопарк» майже гарантовано затягне реліз і ускладнить налагодження.

1
Опитування
Локалізація, рівень 9, лекція 4
Недоступний
Локалізація
Локалізація (UI, дані, описи функцій)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ