JavaRush /Курсы /ChatGPT Apps /MCP Gateway и архитектура локализации: одноязычные сервер...

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

ChatGPT Apps
9 уровень , 4 лекция
Открыта

1. Зачем вообще думать об архитектуре локализации

Пока у вас один язык и маленький каталог, всё просто: вы храните gift_catalog.json, все тексты на русском, а MCP‑сервер честно выдаёт эти подарки всем подряд. Но как только вы хотите:

  • английский UI для США и Европы,
  • отдельный русскоязычный каталог с книгами на русском,
  • разные рынки,

наивный подход «в каждом хендлере ещё один if (locale === "ru")» начинает превращать код в рождественскую ёлку.

MCP — это, с одной стороны, протокол, а с другой — серверная реализация этого протокола. Сервер получает запросы от ChatGPT с метаданными, в том числе locale и userLocation. Вопрос не в том, «умеет ли он прочитать locale», а в том, где именно в архитектуре вы учитываете этот сигнал. Можно в каждом инструменте, а можно вынести часть логики в отдельный слой — Gateway.

Хорошая архитектура локализации должна отвечать на три вопроса:

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

Сегодня мы как раз это и разберём.

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

Прежде чем решать, где именно в архитектуре учитывать locale, полезно вспомнить, как выглядит MCP‑запрос на уровне протокола и какие метаданные платформа уже передаёт.

Напомню важный факт: MCP‑запросы — это JSON‑RPC‑сообщения. Каждое сообщение само по себе, протокол не навязывает вам stateful‑сессию. Поэтому, если вы хотите, чтобы сервер учитывал локаль, её нужно либо:

  • передавать явно как аргумент инструмента (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.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 В коде инструментов
Деплой и масштабирование Проще, одна точка
Локализация каталогов Через условную загрузку файлов/запросов
Кода if (locale ...) Становится много
Поддержка разных рынков/API Весь «зоопарк» в одном коде

Эта модель отлично подходит для:

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

Она хуже подходит, когда:

  • языков становится много,
  • команды и данные для разных рынков принципиально разные (отдельные БД, свои e‑commerce API, свои легальные требования).

И как раз в таких случаях на сцену выходит вторая модель.

4. Модель 2: MCP Gateway + одноязычные backend‑серверы

Теперь представим, что GiftGenius работает и в США, и в Болгарии, и, допустим, в Германии. Для США вы ходите в Amazon API, для Болгарии — в eMAG, для Германии — в локального ритейлера. У каждого рынка свой контракт, свои особенности, своя команда. Пихать всё в один MCP‑монолит неприятно.

Идея модели 2 такая:

Между ChatGPT и реальными MCP‑сервисами стоит Gateway. Для ChatGPT это просто ещё один MCP‑сервер, а внутри он маршрутизирует запросы к разным backend‑серверам, каждый из которых «говорит» только на одном языке и работает с одним рынком.

Как это выглядит на диаграмме

Сначала нарисуем сравнение двух моделей.

flowchart TB
subgraph Model1["Модель 1: Один MCP"]
  A1[ChatGPT] --> B1["GiftGenius MCP (мультиязычный)"]
end

subgraph Model2["Модель 2: Gateway + мономы"]
  A2[ChatGPT] --> G[MCP Gateway]
  G --> B["GiftGenius MCP BG (bg-BG, eMAG)"]
  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 или eMAG, остаются внутри конкретного языкового MCP‑сервера. Gateway не знает, как выглядит «идеальный подарок тёще». Ему достаточно знать, что для ru-RU нужно идти в mcp-giftgenius-ru, а для 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];     // 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"
    // Вызываем тот же инструмент на нужном 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
Сложность инфраструктуры Минимальная Выше: нужны отдельные deploy для каждого языка
Изоляция по рынкам Низкая: один код, один процесс Высокая: падение RU‑сервера не ломает EN и наоборот
Поддержка разных команд Сложнее разделить ответственность Естественно: команда RU, EN, DE могут пилить свои MCP отдельно
Логика локализации в коде Смешана с бизнес‑логикой в каждом хендлере Сконцентрирована в Gateway и в границах конкретного backend‑сервиса

Для нашего учебного курса мы будем в основном придерживаться модели 1 (один MCP + locale как параметр), а модель с Gateway будем рассматривать как естественный путь масштабирования, когда у вас уже «настоящий бизнес» с десятками рынков. Тем не менее, раз Gateway — естественный следующий шаг, мы посмотрим на одну важную деталь этой архитектуры: как хранить locale и страну пользователя в состоянии сессии.

6. Locale как часть состояния клиента в Gateway

До сих пор мы предполагали, что каждый запрос содержит всё, что нужно. Но в реальной жизни удобно часть информации держать в состоянии сессии. Например:

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

У MCP есть полезное поле _meta["openai/subject"] — анонимный идентификатор пользователя, который OpenAI шлёт в ваши сервисы. Его можно использовать как ключ сессии.

Простая реализация состояния в памяти

Напишем крошечный state‑слой в 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"] = "bg-BG",
  • _meta["openai/userLocation"].country = "BG".

Пользователь пишет: «Избери подарък за колега, харесва настолни игри, до 100 лева».

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

  1. Хендлер читает locale из _meta, получает "bg-BG".
  2. Загружает gift_catalog.bg.json, где все названия на болгарском, цены в левах.
  3. Фильтрует по категории и бюджету, возвращает структурированный список подарков на болгарском.

В модели 2 (Gateway + мономы):

  1. Gateway читает locale и userLocation, решает, что это BG‑пользователь.
  2. Направляет вызов suggest_gifts на mcp-giftgenius-bg.
  3. Тот работает только с болгарским каталогом и eMAG 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 в схемах инструментов 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 ru-RU"
    },
    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 и на этом основании решать, какой сервер вызвать. Это может быть полезным fallback‑ом, но не основным механизмом. Платформа уже даёт вам 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.
Иногда хочется сделать просто: «если страна BG — идём на BG‑сервер». Но пользователь может находиться в Германии и всё равно хотеть интерфейс на болгарском, или он может попросить «switch to English» посреди сессии. Если вы в Gateway не учитываете openai/locale и возможное желание пользователя поменять язык, роутинг становится «бетонным» и ломает UX. Лучше опираться на комбинацию locale и userLocation, а также держать возможность переопределения настроек через состояние сессии.

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

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

1
Задача
ChatGPT Apps, 9 уровень, 4 лекция
Недоступна
Локализация описаний tool и JSON Schema (двуязычные descriptions)
Локализация описаний tool и JSON Schema (двуязычные descriptions)
1
Опрос
Локализация, 9 уровень, 4 лекция
Недоступен
Локализация
Локализация (UI, данные, описания функций)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ