1. Por que pensar na arquitetura de localização
Enquanto você tem um único idioma e um catálogo pequeno, tudo é simples: você guarda gift_catalog.json, todos os textos em russo, e o servidor MCP entrega esses presentes para todo mundo. Mas assim que você quer:
- UI em inglês para os EUA e a Europa,
- um catálogo separado em russo com matrioshkas e livros em russo,
- mercados diferentes (Amazon para os EUA, Ozon para a Rússia),
a abordagem ingênua “em cada handler mais um if (locale === "ru")” começa a transformar o código numa árvore de Natal.
MCP é, por um lado, um protocolo e, por outro, uma implementação de servidor desse protocolo. O servidor recebe solicitações do ChatGPT com metadados, incluindo locale e userLocation. A questão não é “se ele sabe ler o locale”, mas sim onde exatamente na arquitetura você leva em conta esse sinal. Dá para fazer isso em cada ferramenta, ou mover parte da lógica para uma camada separada — o Gateway.
Uma boa arquitetura de localização deve responder a três perguntas:
- Onde tomamos a decisão de qual idioma e região usar.
- Onde escolhemos os dados e integrações necessários (catálogos, APIs de lojas, moedas).
- Onde e como armazenamos o estado do usuário (locale, moeda e, possivelmente, algumas preferências), para não ter que passar isso manualmente toda vez.
É exatamente isso que vamos analisar hoje.
2. MCP, _meta e a natureza stateless: por que o locale precisa ser passado explicitamente
Antes de decidir onde exatamente considerar o locale na arquitetura, é útil lembrar como se parece uma solicitação MCP no nível do protocolo e quais metadados a plataforma já envia.
Um fato importante: as solicitações MCP são mensagens JSON‑RPC. Cada mensagem é independente; o protocolo não impõe uma sessão stateful. Portanto, se você quer que o servidor considere a localidade, é preciso ou:
- passá-la explicitamente como argumento da ferramenta (locale no inputSchema), ou
- lê-la de _meta["openai/locale"], que o ChatGPT adiciona à solicitação.
Um exemplo simples de handler que lê o 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;
// Em seguida, usamos locale e country para escolher o catálogo
const gifts = await loadGiftCatalog(locale, country);
return { structuredContent: { gifts } };
}
);
Aqui não propagamos o locale pelos argumentos; confiamos em _meta, que o SDK já colocou em extra. É uma opção totalmente válida e que será útil no primeiro modelo — com um único MCP multilíngue.
No segundo modelo — com Gateway — o _meta também desempenha um papel fundamental: o gateway lê o locale dos metadados e, com base nisso, decide para onde encaminhar a solicitação. Vamos analisar separadamente abaixo em que formato manter o locale — apenas no _meta ou também nos schemas das ferramentas.
3. Modelo 1: um servidor MCP multilíngue (“monólito poliglota”)
Comecemos pela opção arquitetural mais simples. Você tem um servidor MCP, uma URL, um deploy, uma base de código. Dentro de cada ferramenta você:
- Obtém o locale (de _meta ou de um argumento).
- Com base no locale, escolhe os recursos necessários: gift_catalog.en.json, gift_catalog.ru.json e assim por diante.
- Devolve o resultado já no idioma desejado.
Exemplo para o GiftGenius
Suponha que temos dois arquivos de catálogo:
- data/gift_catalog.en.json
- data/gift_catalog.ru.json
Vamos criar um pequeno helper loadGiftCatalog(locale), que escolhe o arquivo correto:
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 presentes
}
Agora nossa ferramenta suggest_gifts pode simplesmente chamar esse helper:
server.registerTool(
"suggest_gifts",
{ title: "Sugerir presentes", 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 } };
}
);
O resultado é que a localização fica escondida em um único lugar — em loadGiftCatalog —, e as ferramentas apenas passam para lá o locale. Da mesma forma, é possível ajustar formatos de data, moedas e quaisquer outras particularidades regionais.
Prós e contras desse modelo
Para não nos estendermos demais, vamos resumir os prós e contras do primeiro modelo em uma pequena tabela (por enquanto apenas sobre “um MCP” — a comparação com o Gateway virá depois).
| Critério | Um MCP multilíngue |
|---|---|
| Quantidade de instâncias MCP | 1 |
| Onde o locale é considerado | No código das ferramentas |
| Deploy e escalabilidade | Mais simples, um único ponto |
| Localização de catálogos | Por meio de carregamento condicional de arquivos/consultas |
| Código if (locale ...) | Aumenta muito |
| Suporte a diferentes mercados/APIs | Todo o “zoológico” em um único código |
Esse modelo é ótimo para:
- MVPs e aplicativos pequenos, com 2–3 idiomas e mercados não tão diferentes;
- projetos educacionais (por exemplo, nosso GiftGenius no contexto do curso).
É menos adequado quando:
- o número de idiomas aumenta,
- as equipes e os dados para diferentes mercados são fundamentalmente distintos (BDs separadas, APIs de e‑commerce próprias, requisitos legais próprios).
E é justamente nesses casos que entra o segundo modelo.
4. Modelo 2: MCP Gateway + servidores backend monolíngues
Agora imagine que o GiftGenius funciona nos EUA, na Rússia e, digamos, na Alemanha. Para os EUA você usa a Amazon API, para a Rússia — o Ozon, para a Alemanha — um varejista local. Cada mercado tem seu contrato, suas particularidades e sua equipe. Enfiar tudo em um único monólito MCP é desagradável.
A ideia do modelo 2 é a seguinte:
Entre o ChatGPT e os serviços MCP reais há um Gateway. Para o ChatGPT, ele é apenas mais um servidor MCP; por dentro, ele roteia as solicitações para diferentes servidores de backend, cada um dos quais “fala” apenas um idioma e atua em um único mercado.
Como isso fica no diagrama
Primeiro, vamos desenhar a comparação dos dois modelos.
flowchart LR
subgraph Model1["Modelo 1: Um MCP"]
A1[ChatGPT] --> B1["GiftGenius MCP (multilíngue)"]
end
subgraph Model2["Modelo 2: Gateway + monolíngues"]
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, Loja local)"]
end
Aos olhos do ChatGPT, no segundo modelo existe apenas um endpoint MCP — o Gateway. Internamente, ele analisa _meta["openai/locale"] e/ou _meta["openai/userLocation"] e escolhe o backend correto.
O que o Gateway faz (no contexto desta aula)
É importante não transformar o Gateway em “um segundo monólito com toda a lógica de negócio”. No nosso módulo, o papel dele é bem limitado:
- Receber a mensagem MCP do ChatGPT (incluindo _meta).
- Extrair locale / userLocation.
- Com base nisso, escolher o servidor de backend necessário.
- Fazer o proxy da solicitação (JSON‑RPC) e retornar a resposta.
Todas as decisões sobre qual catálogo de presentes usar, como chamar a Amazon ou o Ozon, permanecem dentro do MCP específico daquele idioma/mercado. O Gateway não sabe qual é “o presente ideal para a sogra”. Basta saber que para ru-RU deve chamar mcp-giftgenius-ru, e para en-US — mcp-giftgenius-en.
Esqueleto mais simples de um MCP Gateway em TypeScript
Vamos simplificar bastante para não afundar em detalhes. Suponha que temos um helper callDownstreamTool, que sabe se comunicar com os servidores MCP internos via JSON‑RPC (poderia ser via HTTP ou uma conexão SSE persistente, mas deixaremos os detalhes para o módulo 16).
import { Server } from "@modelcontextprotocol/sdk/server";
const server = new Server({ name: "giftgenius-gateway" });
function chooseBackend(locale?: string) {
if (!locale) return "en"; // padrão
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"
// Chamamos a mesma ferramenta no backend adequado
return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
}
);
Os servidores MCP internos registram suggest_gifts com exatamente o mesmo contrato, mas cada um trabalha apenas com seu idioma/mercado e não sabe que existem outros idiomas.
Da mesma forma, o Gateway pode fazer proxy de listTools, listResources e outros métodos MCP, mas isso já é tema para outro módulo.
5. Comparação dos dois modelos para localização
Antes analisamos separadamente os prós e contras do modelo “um MCP”. Agora vamos reunir as diferenças dos dois modelos pelos principais parâmetros.
| Critério | Um MCP multilíngue | Gateway + MCPs monolíngues |
|---|---|---|
| Quantidade de serviços MCP | 1 | 1 Gateway + N servidores de backend |
| Onde o locale é considerado | Dentro de cada ferramenta (lógica if locale ...) | No Gateway, que faz o roteamento; dentro dos serviços, o idioma é fixo |
| Flexibilidade de UX (troca de idioma) | Fácil, tudo em um lugar; o LLM apenas altera o locale | Possível, mas é preciso pensar como o Gateway trocará o backend |
| Complexidade de infraestrutura | Mínima | Maior: é preciso deploys separados para cada idioma |
| Isolamento por mercados | Baixo: um único código, um único processo | Alto: queda do servidor RU não quebra o EN e vice‑versa |
| Suporte a equipes diferentes | Mais difícil dividir a responsabilidade | Natural: equipes RU, EN, DE podem desenvolver seus MCPs separadamente |
| Lógica de localização no código | Misturada com a lógica de negócio em cada handler | Concentrada no Gateway e nas fronteiras de cada serviço de backend |
Para nosso curso, seguiremos principalmente o modelo 1 (um MCP + locale como parâmetro), e o modelo com Gateway será considerado como um caminho natural de escala quando você já tem “um negócio de verdade” com dezenas de mercados. Ainda assim, como o Gateway é um próximo passo natural, veremos um detalhe importante dessa arquitetura: como manter locale e país do usuário no estado da sessão.
6. Locale como parte do estado do cliente no Gateway
Até agora supusemos que cada solicitação contém tudo o que é necessário. Mas, na vida real, é conveniente manter parte das informações no estado da sessão. Por exemplo:
- o usuário chega uma vez com locale = "ru-RU" e userLocation.country = "RU";
- depois você quer rotear todas as solicitações dele para o backend RU, mesmo que algumas chamadas intermediárias cheguem sem um locale explícito nos argumentos.
O MCP tem o campo útil _meta["openai/subject"] — um identificador anônimo de usuário que a OpenAI envia aos seus serviços. Você pode usá-lo como chave de sessão.
Implementação simples de estado em memória
Vamos escrever uma camada de estado mínima no Gateway (claro que, em produção, em vez de Map é melhor usar Redis ou outro armazenamento 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);
}
Agora, no handler do Gateway, podemos primeiro atualizar o estado e depois usá‑lo para escolher o servidor de 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);
}
);
Dessa forma, você “memoriza” uma vez o mapeamento clientId → locale, country e pode usá‑lo em todas as chamadas subsequentes das ferramentas, sem copiar campos em cada argumento.
O Gateway também pode lembrar a moeda preferida, o formato de preços ou outras configurações que serão úteis para a lógica de comércio (mas isso já é assunto mais para o módulo sobre ACP).
7. GiftGenius: dois cenários e o impacto da escolha arquitetural
Para não parecer que estamos apenas discutindo quadrinhos abstratos, vejamos cenários concretos do GiftGenius.
Cenário 1: Usuário da Rússia, escreve em russo
Temos:
- _meta["openai/locale"] = "ru-RU",
- _meta["openai/userLocation"].country = "RU".
O usuário escreve: “Escolha um presente para um colega, ele gosta de jogos de tabuleiro, até 3.000 rublos”.
No modelo 1 (um MCP):
- O handler lê o locale de _meta e obtém "ru-RU".
- Carrega gift_catalog.ru.json, em que todos os nomes estão em russo e os preços em rublos.
- Filtra por categoria e orçamento e retorna uma lista estruturada de presentes em russo.
No modelo 2 (Gateway + monolíngues):
- O Gateway lê locale e userLocation e decide que é um usuário RU.
- Encaminha a chamada suggest_gifts para mcp-giftgenius-ru.
- Esse trabalha apenas com o catálogo em russo e a Ozon API, devolvendo presentes em rublos.
Em ambos os casos, o usuário vê tudo no idioma nativo, mas, no segundo, seu servidor MCP em inglês nem sabe da existência do catálogo para a Rússia.
Cenário 2: Usuário da Alemanha, escreve em inglês
Agora:
- _meta["openai/locale"] = "en",
- _meta["openai/userLocation"].country = "DE".
O usuário escreve: “Gift for my German coworker, budget 50 EUR”.
No modelo 1:
- o locale "en" fornece textos em inglês,
- e o country "DE" pode ser usado para escolher o catálogo com preços em euros e sortimento adaptado à Europa.
No modelo 2:
- O Gateway pode decidir que locale = "en" → serviço em inglês, mas country = "DE" → produtos do estoque europeu; dependendo da sua lógica de negócio, você pode:
- ou encaminhar a solicitação para mcp-giftgenius-en com o parâmetro country=DE,
- ou ter um mcp-giftgenius-eu separado para a Europa.
Fica claro aqui que localidade (idioma) e região (userLocation) são dimensões diferentes, e o Gateway é um lugar conveniente para combiná‑las na decisão “qual serviço chamar e quais produtos mostrar”.
8. Locale nos schemas das ferramentas vs. locale apenas em _meta
Independentemente de você usar um MCP único ou o conjunto Gateway + serviços monolíngues, vale discutir um ponto sutil, porém importante: manter o locale apenas em _meta ou torná‑lo um argumento da ferramenta?
Há duas abordagens.
Primeira: confiar apenas no _meta.
Isso é conveniente porque os schemas das ferramentas não ficam poluídos com mais um campo. O servidor lê o locale de extra._meta e decide por conta própria. No modelo 1, isso costuma ser suficiente.
Segunda: adicionar explicitamente locale (e, possivelmente, currency) no inputSchema da ferramenta.
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"]
};
Depois, no system‑prompt, você pode pedir ao modelo para sempre preencher o argumento locale, usando o valor do contexto do usuário. Isso deixa as intenções transparentes: nos argumentos JSON, fica explícito em qual idioma o servidor deve atuar. Essa abordagem é especialmente útil numa arquitetura mais complexa, na qual existe um MCP comum que, com base no locale, roteia para diferentes serviços ou recursos.
Na prática, muitas vezes se combinam as duas abordagens: há o campo locale nos schemas, mas, se por algum motivo o modelo não o preencher, o servidor se garante com _meta["openai/locale"].
9. Onde está a fronteira entre localização e “lógica demais” no Gateway
Uma armadilha comum: já que temos um Gateway esperto, vamos fazer com que ele:
- decida por conta própria quais presentes mostrar,
- formate datas e preços,
- monte relatórios de cliques e assim por diante.
Isso parece tentador, mas transforma o Gateway em “um segundo monólito” e complica sua atualização e operação. Nas práticas industriais de API gateways (e o MCP Gateway, em essência, é um gateway), o foco é mantido em algumas tarefas: autenticação, autorização, roteamento e enriquecimento leve de contexto. Por exemplo, o gateway pode transformar cabeçalhos HTTP em metadados convenientes. A lógica de negócio e as operações pesadas devem viver nos serviços de backend.
Para localização, isso significa:
- O Gateway pode fazer o parsing de _meta["openai/locale"] e _meta["openai/userLocation"].
- Pode lembrá‑los no estado do cliente.
- Pode escolher o servidor de idioma apropriado ou adicionar ao pedido um campo locale/country.
Mas a própria seleção de presentes, filtragem por idade, orçamento etc. — tudo isso deve permanecer nos backends MCP.
10. Erros comuns ao projetar localização com MCP e Gateway
Erro nº 1: Confiar apenas em “adivinhar” o idioma pelo texto do usuário.
Às vezes, dá vontade de pegar o texto da mensagem, passar por um detector de idioma e, com base nisso, decidir qual servidor chamar. Isso pode ser um fallback útil, mas não o mecanismo principal. A plataforma já fornece openai/locale e openai/userLocation, que consideram as configurações do ChatGPT e o ambiente do usuário. Ignorar esses sinais e “adivinhar o idioma” é um caminho eficaz para quebrar o UX nos casos mais inesperados.
Erro nº 2: Manter o locale apenas “na cabeça” do modelo e não passá‑lo ao servidor.
Se locale não aparece nem em _meta nem nos argumentos da ferramenta, o servidor não sabe nada sobre o idioma do usuário. O modelo até pode tentar traduzir a string “книги” para books, mas isso não é confiável, especialmente se você tiver categorias complexas. O caminho certo é passar o locale explicitamente: ou via o argumento locale, ou lendo de _meta e construindo a arquitetura em torno disso.
Erro nº 3: Transferir toda a lógica de negócio de localização para o Gateway.
Se o Gateway começa a escolher presentes por conta própria, acessar bancos de dados e brigar com APIs externas, ele deixa de ser um roteador leve e vira um serviço pesado, difícil de escalar e atualizar. No fim, você ganha dois monólitos em vez de um. É melhor manter o Gateway o mais “simples” possível: ele observa locale/userLocation, escolhe o backend certo e encaminha os metadados com cuidado.
Erro nº 4: Amarrar rigidamente o roteamento apenas a IP ou userLocation.
Às vezes, dá vontade de fazer o simples: “se o país é RU — vai para o servidor RU”. Mas o usuário pode estar na Alemanha e ainda querer a interface em russo, ou pode pedir “switch to English” no meio da sessão. Se, no Gateway, você não considerar openai/locale e a possível vontade do usuário de mudar de idioma, o roteamento fica “engessado” e quebra o UX. É melhor basear‑se na combinação de locale e userLocation e manter a possibilidade de sobrescrever ajustes via estado da sessão.
Erro nº 5: Não usar _meta["openai/subject"] e duplicar todos os parâmetros em cada argumento.
Quando você começa a carregar em cada argumento de ferramenta locale, country, currency, userId e mais meio formulário, a vida fica rapidamente triste. O MCP já transmite um identificador anônimo do usuário por meio de _meta["openai/subject"], e você pode armazenar todas essas informações no estado do cliente no Gateway ou no servidor de backend. Isso simplificará os contratos e reduzirá o risco de dessincronização de argumentos.
Erro nº 6: Falta de estratégia de evolução: “construímos logo um Gateway complexo para dez idiomas”.
Muitas vezes dá vontade de fazer tudo perfeito desde o início: Gateway, cinco idiomas, três regiões, dez serviços MCP. Na prática, é mais simples começar com o modelo “um MCP + parâmetro de locale ou _meta”, estabilizar o comportamento e, depois, destacar o Gateway e os serviços monolíngues à medida que você cresce. Tentar construir um “zoológico” enorme de primeira quase certamente atrasará o release e dificultará a depuração.
GO TO FULL VERSION