CodeGym /Cursos /ChatGPT Apps /MCP Gateway y arquitectura de localización: servidores mo...

MCP Gateway y arquitectura de localización: servidores monolingües, locale como parámetro, estado del cliente

ChatGPT Apps
Nivel 9 , Lección 4
Disponible

1. Por qué pensar en la arquitectura de la localización

Mientras tengas un idioma y un catálogo pequeño, todo es simple: guardas gift_catalog.json, todos los textos en ruso, y el servidor MCP devuelve esos regalos a todo el mundo por igual. Pero en cuanto quieres:

  • una interfaz en inglés para EE. UU. y Europa,
  • un catálogo separado en ruso con matrioshkas y libros en ruso,
  • mercados distintos (Amazon para EE. UU., Ozon para Rusia),

el enfoque ingenuo de «en cada manejador otro if (locale === "ru")» empieza a convertir el código en un árbol de Navidad.

MCP es, por un lado, un protocolo y, por otro, una implementación de servidor de ese protocolo. El servidor recibe solicitudes de ChatGPT con metadatos, entre ellos locale y userLocation. La cuestión no es «si sabe leer el locale», sino dónde exactamente en la arquitectura tienes en cuenta esa señal. Puedes hacerlo en cada herramienta, o puedes extraer parte de la lógica a una capa aparte — el Gateway.

Una buena arquitectura de localización debe responder a tres preguntas:

  1. Dónde tomamos la decisión de qué idioma y región usar.
  2. Dónde elegimos los datos e integraciones necesarios (catálogos, API de tiendas, divisas).
  3. Dónde y cómo almacenamos el estado del usuario (locale, divisa y, posiblemente, algunas preferencias), para no pasar esto manualmente cada vez.

Eso es lo que vamos a ver hoy.

2. MCP, _meta y la naturaleza stateless: por qué hay que pasar el locale de forma explícita

Antes de decidir dónde exactamente en la arquitectura tener en cuenta el locale, conviene recordar cómo se ve una solicitud MCP a nivel de protocolo y qué metadatos ya proporciona la plataforma.

Un hecho importante: las solicitudes MCP son mensajes JSON‑RPC. Cada mensaje es autónomo; el protocolo no impone una sesión con estado. Por tanto, si quieres que el servidor tenga en cuenta la configuración regional, debes:

  • pasarla explícitamente como argumento de la herramienta (locale en el inputSchema), o
  • leerla de _meta["openai/locale"], que ChatGPT añade a la solicitud.

Un ejemplo muy simple de manejador que lee el locale de _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;

    // A continuación usamos locale y country para seleccionar el catálogo
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

Aquí no pasamos el locale a través de los argumentos, sino que nos basamos en _meta, que el SDK ya colocó en extra. Esta opción funciona bien y nos será útil en el primer modelo — con un MCP multilingüe.

En el segundo modelo — con Gateway — _meta también desempeña un papel clave: el gateway lee el locale de los metadatos y, en función de ello, decide adónde enviar la solicitud. En qué forma almacenar exactamente el locale — solo en _meta o también en los esquemas de las herramientas — lo veremos en un bloque aparte más abajo.

3. Modelo 1: un único servidor MCP multilingüe («políglota‑monolito»)

Empecemos por la variante arquitectónica más simple. Tienes un único servidor MCP, una única URL, un único despliegue, una única base de código. Dentro de cada herramienta:

  1. Obtienes el locale (desde _meta o como argumento).
  2. En función del locale eliges los recursos necesarios: gift_catalog.en.json, gift_catalog.ru.json, etc.
  3. Devuelves el resultado ya en el idioma adecuado.

Ejemplo para GiftGenius

Supongamos que tenemos dos archivos de catálogos:

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

Hagamos un pequeño helper loadGiftCatalog(locale), que elija el archivo adecuado:

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; // array de regalos
}

Ahora nuestra herramienta suggest_gifts puede simplemente llamar a este helper:

server.registerTool(
  "suggest_gifts",
  { title: "Selección de regalos", 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 } };
  }
);

De este modo, la localización queda encapsulada en un solo sitio — en loadGiftCatalog, y las herramientas simplemente le pasan el locale. De la misma forma, puedes seleccionar formatos de fecha, divisas y cualquier otra cuestión dependiente de la región.

Ventajas e inconvenientes de este modelo

Para no perdernos en texto, resumamos las ventajas e inconvenientes de este primer modelo en una pequeña tabla (de momento solo sobre «un único MCP» — volveremos a la comparación con Gateway más adelante).

Criterio Un MCP multilingüe
Número de instancias MCP 1
Dónde se tiene en cuenta el locale En el código de las herramientas
Despliegue y escalado Más sencillo, un único punto
Localización de catálogos Mediante carga condicional de archivos/consultas
Código de if (locale ...) Acaba siendo mucho
Soporte de mercados/API distintos Todo el «zoológico» en un mismo código

Este modelo encaja muy bien para:

  • MVP y aplicaciones pequeñas con 2–3 idiomas y mercados no demasiado diferentes;
  • proyectos didácticos (por ejemplo, nuestro GiftGenius en el marco del curso).

Encaja peor cuando:

  • los idiomas se multiplican,
  • los equipos y los datos para distintos mercados difieren de forma sustancial (BD separadas, sus propias API de e‑commerce, requisitos legales propios).

Y justamente en esos casos entra en escena el segundo modelo.

4. Modelo 2: MCP Gateway + servidores backend monolingües

Ahora imaginemos que GiftGenius funciona en EE. UU., en Rusia y, por ejemplo, en Alemania. Para EE. UU. llamas al Amazon API, para Rusia — a Ozon, para Alemania — a un minorista local. Cada mercado tiene su propio contrato, sus particularidades, su equipo. Meterlo todo en un único MCP monolítico no resulta agradable.

La idea del modelo 2 es la siguiente:

Entre ChatGPT y los servicios MCP reales se sitúa un Gateway. Para ChatGPT es simplemente otro servidor MCP, y por dentro enruta las solicitudes a distintos servidores backend, cada uno de los cuales «habla» solo un idioma y trabaja con un único mercado.

Cómo se ve en un diagrama

Primero dibujemos la comparación de los dos modelos.

flowchart LR
    subgraph Model1["Modelo 1: Un MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (multilingüe)"]
    end

    subgraph Model2["Modelo 2: Gateway + monolingües"]
      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, tienda local)"]
    end

A ojos de ChatGPT, en el segundo modelo solo hay un endpoint MCP — el Gateway. Por dentro, analiza _meta["openai/locale"] y/o _meta["openai/userLocation"] y elige el backend correcto.

Qué hace el Gateway (en el contexto de esta lección)

Es importante no convertir el Gateway en «un segundo monolito con toda la lógica de negocio». En nuestro módulo su papel está muy limitado:

  1. Recibir el mensaje MCP de ChatGPT (incluido _meta).
  2. Extraer el locale / userLocation.
  3. Con base en ello, elegir el servidor backend adecuado.
  4. Proxiar allí la solicitud (JSON‑RPC) y devolver la respuesta.

Todas las decisiones sobre qué catálogo de regalos tomar, cómo exactamente llamar a Amazon u Ozon, permanecen dentro del servidor MCP de cada idioma. El Gateway no sabe cuál es «el regalo perfecto para la suegra». Le basta con saber que para ru-RU debe ir a mcp-giftgenius-ru, y para en-US — a mcp-giftgenius-en.

Esqueleto mínimo de un MCP Gateway en TypeScript

Simplifiquemos mucho para no perdernos en detalles. Imaginemos que tenemos un helper callDownstreamTool, que sabe comunicarse con servidores MCP internos vía JSON‑RPC (podrían ser peticiones HTTP o una conexión SSE persistente, pero dejaremos los detalles para el módulo 16).

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

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

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // por defecto
  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"
    // Llamamos a la misma herramienta en el servidor backend apropiado
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

Los servidores MCP internos registran suggest_gifts con exactamente el mismo contrato, pero cada uno trabaja solo con su idioma/mercado y no sabe que existen otros idiomas.

De la misma forma, el Gateway puede proxiar listTools, listResources y otros métodos MCP, pero eso ya es tema de otro módulo.

5. Comparación de los dos modelos para localización

Antes hemos visto por separado las ventajas e inconvenientes del modelo «un único MCP». Ahora reunimos las diferencias de ambos modelos según parámetros clave.

Criterio Un MCP multilingüe Gateway + servidores MCP monolingües
Número de servicios MCP 1 1 Gateway + N servidores backend
Dónde se tiene en cuenta el locale Dentro de cada herramienta (lógica de if locale ...) En el Gateway, que enruta; dentro de los servicios el idioma es fijo
Flexibilidad de UX (cambio de idioma) Fácil, todo en un solo sitio; el LLM solo cambia el locale Posible, pero hay que pensar cómo hará el Gateway el cambio de backend
Complejidad de infraestructura Mínima Mayor: despliegues separados para cada idioma
Aislamiento por mercados Bajo: un mismo código, un mismo proceso Alto: la caída del servidor RU no rompe EN y viceversa
Soporte de equipos distintos Más difícil dividir responsabilidades Natural: los equipos RU, EN, DE pueden desarrollar sus MCP por separado
Lógica de localización en el código Mezclada con la lógica de negocio en cada manejador Concentrada en el Gateway y en los límites de cada servicio backend

Para nuestro curso, nos ceñiremos principalmente al modelo 1 (un único MCP + locale como parámetro), y veremos el modelo con Gateway como una vía natural de escalado cuando ya tengas un «negocio real» con decenas de mercados. Aun así, dado que el Gateway es un paso natural, veremos un detalle importante de esta arquitectura: cómo almacenar el locale y el país del usuario en el estado de la sesión.

6. El locale como parte del estado del cliente en el Gateway

Hasta ahora hemos supuesto que cada solicitud contiene todo lo necesario. Pero en la vida real es cómodo mantener parte de la información en el estado de la sesión. Por ejemplo:

  • el usuario llega una vez con locale = "ru-RU" y userLocation.country = "RU";
  • después quieres enrutar todas sus solicitudes al backend RU, incluso si algunas llamadas intermedias llegan sin un locale explícito en los argumentos.

MCP tiene un campo útil _meta["openai/subject"] — un identificador anónimo del usuario que OpenAI envía a tus servicios. Puedes usarlo como clave de sesión.

Implementación sencilla de estado en memoria

Escribamos una pequeña capa de estado en el Gateway (por supuesto, en producción en lugar de Map es mejor usar Redis u otro almacén externo).

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

Ahora, en el manejador del Gateway se puede primero actualizar el estado y luego usarlo para elegir el servidor 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);
  }
);

Así, «recuerdas» una vez el mapeo clientIdlocale, country y puedes usarlo en todas las llamadas posteriores a herramientas, sin copiar campos en cada argumento.

De la misma manera, el Gateway puede recordar la divisa preferida, el formato de precios u otros ajustes útiles para la lógica de comercio (pero de esto hablaremos más en el módulo sobre ACP).

7. GiftGenius: dos escenarios y el impacto de la arquitectura elegida

Para que no parezca que solo hablamos de cuadritos abstractos, veamos escenarios concretos de GiftGenius.

Escenario 1: Usuario en Rusia, escribe en ruso

Supongamos que tenemos:

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

El usuario escribe: «Elige un regalo para un compañero, le gustan los juegos de mesa, hasta 3000 rublos».

En el modelo 1 (un MCP):

  1. El manejador lee el locale de _meta y obtiene "ru-RU".
  2. Carga gift_catalog.ru.json, donde todos los nombres están en ruso y los precios en rublos.
  3. Filtra por categoría y presupuesto, y devuelve una lista estructurada de regalos en ruso.

En el modelo 2 (Gateway + monolingües):

  1. El Gateway lee locale y userLocation y decide que es un usuario RU.
  2. Dirige la llamada suggest_gifts a mcp-giftgenius-ru.
  3. Este trabaja solo con el catálogo ruso y el Ozon API, y devuelve regalos en rublos.

En ambos casos el usuario lo ve todo en su idioma, pero en la segunda variante tu servidor MCP en inglés ni siquiera sabe que existe un catálogo para Rusia.

Escenario 2: Usuario en Alemania, escribe en inglés

Ahora:

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

El usuario escribe: «Gift for my German coworker, budget 50 EUR».

En el modelo 1:

  • locale "en" produce textos en inglés,
  • y "DE" en country puede usarse para elegir un catálogo con precios en euros y surtido adaptado a Europa.

En el modelo 2:

  • El Gateway puede decidir que locale = "en" → servicio en inglés, pero country = "DE" → productos del almacén europeo; según tu lógica de negocio puedes:
  • o dirigir la solicitud a mcp-giftgenius-en con el parámetro country=DE,
  • o tener un mcp-giftgenius-eu separado para Europa.

Aquí se ve claramente que el locale (idioma) y la región (userLocation) son dimensiones distintas, y el Gateway es un lugar conveniente para combinarlas en la decisión «qué servicio llamar y qué productos mostrar».

8. Locale en los esquemas de herramientas vs. locale solo en _meta

Independientemente de si usas un único MCP o el conjunto Gateway + servicios monolingües, al final conviene discutir un punto sutil pero importante: ¿guardar el locale solo en _meta o convertirlo en argumento de la herramienta?

Hay dos enfoques.

Primero: confiar solo en _meta.

Es cómodo porque los esquemas de las herramientas no se ensucian con otro campo más. El servidor lee el locale de extra._meta y decide por su cuenta. En el modelo 1 suele ser suficiente.

Segundo: añadir explícitamente locale (y, quizá, currency) al inputSchema de la herramienta.

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

Después, en el system‑prompt puedes pedirle al modelo que siempre rellene el argumento locale usando el valor del contexto del usuario. Esto hace que las intenciones sean transparentes: en los argumentos JSON se ve directamente en qué idioma debe trabajar el servidor. Este enfoque es especialmente útil en arquitecturas más complejas, donde hay un MCP común que enruta internamente según locale a distintos servicios o recursos.

En la práctica, a menudo se combinan ambos enfoques: en los esquemas existe el campo locale, pero si por alguna razón el modelo no lo rellena, el servidor se cubre leyendo _meta["openai/locale"].

9. Dónde está el límite entre la localización y la «lógica sobrante» en el Gateway

La trampa en la que es fácil caer: ya que tenemos un Gateway «inteligente», que sea él quien:

  • decida qué regalos mostrar,
  • formatee fechas y precios,
  • arme informes de clics, etc.

Suena tentador, pero convierte el Gateway en «un segundo monolito» y complica su actualización y operación. En las prácticas industriales de los API gateways (y por rol, un MCP Gateway es lo mismo), se enfocan en varias tareas: autenticación, autorización, enrutamiento y un ligero enriquecimiento del contexto. Por ejemplo, el gateway puede transformar cabeceras HTTP en metadatos prácticos. La lógica de negocio y las operaciones pesadas deben vivir en los servicios backend.

Para la localización esto implica:

  • El Gateway puede parsear _meta["openai/locale"] y _meta["openai/userLocation"].
  • Puede recordarlos en el estado del cliente.
  • Puede elegir el servidor por idioma adecuado o añadir al request el campo locale/country.

Pero la selección de regalos, el filtrado por edad, presupuesto, etc., todo eso debe permanecer en los backend MCP.

10. Errores típicos al diseñar la localización con MCP y Gateway

Error n.º 1: Confiar solo en «adivinar» el idioma a partir del texto del usuario.
A veces apetece tomar el texto del mensaje, pasarlo por un detector de idiomas y decidir con base en eso qué servidor invocar. Puede ser un buen fallback, pero no el mecanismo principal. La plataforma ya te da openai/locale y openai/userLocation, que tienen en cuenta los ajustes de ChatGPT y el entorno del usuario. Ignorar estas señales y jugar a «adivina el idioma» es una forma eficaz de romper la UX en los casos más inesperados.

Error n.º 2: Guardar el locale solo «en la cabeza» del modelo y no pasarlo al servidor.
Si locale no figura ni en _meta ni en los argumentos de la herramienta, el servidor no sabe nada del idioma del usuario. El modelo, claro, puede intentar traducir la cadena «книги» a books, pero eso no es fiable, especialmente si tienes categorías complejas. El camino correcto es pasar el locale explícitamente: o bien como argumento locale, o leyéndolo de _meta y construyendo la arquitectura alrededor de ello.

Error n.º 3: Trasladar toda la lógica de negocio de la localización al Gateway.
Si el Gateway empieza a elegir regalos por su cuenta, a consultar bases de datos y a pelear con API externas, deja de ser un enrutador ligero y se convierte en un servicio pesado, difícil de escalar y actualizar. En resultado, obtienes dos monolitos en lugar de uno. Es mejor mantener el Gateway lo más «tonto» posible: mira locale/userLocation, elige el backend adecuado y transmite los metadatos de forma cuidadosa.

Error n.º 4: Enlazar rígidamente el enrutamiento solo a la IP o a userLocation.
A veces apetece hacerlo simple: «si el país es RU — vamos al servidor RU». Pero el usuario puede estar en Alemania y aun así querer la interfaz en ruso, o puede pedir «switch to English» a mitad de sesión. Si en el Gateway no tienes en cuenta openai/locale y el posible deseo del usuario de cambiar de idioma, el enrutamiento se vuelve «de hormigón» y rompe la UX. Es mejor basarse en la combinación de locale y userLocation, y mantener la posibilidad de sobreescribir ajustes vía el estado de la sesión.

Error n.º 5: No usar _meta["openai/subject"] y duplicar todos los parámetros en cada argumento.
Cuando en cada argumento de herramienta empiezas a arrastrar locale, country, currency, userId y medio interfaz más, la vida se vuelve rápidamente triste. MCP ya pasa un identificador anónimo de usuario mediante _meta["openai/subject"], y puedes guardar toda esa información en el estado del cliente del lado del Gateway o del servidor backend. Esto simplificará los contratos y reducirá el riesgo de desincronización de argumentos.

Error n.º 6: Falta de estrategia de evolución: «construimos de entrada un Gateway complejo con diez idiomas».
A menudo se quiere hacerlo perfecto desde el principio: Gateway, cinco idiomas, tres regiones, diez servicios MCP. En la práctica, es más sencillo empezar con el modelo «un MCP + parámetro de locale o _meta», llevar el comportamiento a algo estable y luego separar el Gateway y los servicios monolingües a medida que creces. Intentar construir de golpe un gran «zoológico» casi siempre retrasa el lanzamiento y complica la depuración.

1
Cuestionario/control
Localización, nivel 9, lección 4
No disponible
Localización
Localización (UI, datos, descripciones de funciones)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION