CodeGym /Corsi /ChatGPT Apps /MCP Gateway e architettura della localizzazione: server m...

MCP Gateway e architettura della localizzazione: server monolingue, locale come parametro, stato del client

ChatGPT Apps
Livello 9 , Lezione 4
Disponibile

1. Perché pensare all’architettura della localizzazione

Finché hai una sola lingua e un catalogo piccolo, tutto è semplice: conservi gift_catalog.json, tutti i testi in russo, e il server MCP restituisce onestamente questi regali a tutti. Ma non appena vuoi:

  • un’interfaccia in inglese per USA ed Europa,
  • un catalogo separato in russo con matrioske e libri in russo,
  • mercati diversi (Amazon per gli USA, Ozon per la Federazione Russa),

l’approccio ingenuo «in ogni handler un altro if (locale === "ru")» inizia a trasformare il codice in un albero di Natale.

MCP è, da un lato, un protocollo e, dall’altro, un’implementazione server di questo protocollo. Il server riceve richieste da ChatGPT con metadati, tra cui locale e userLocation. La domanda non è se «sa leggere la locale», ma dove esattamente nell’architettura tieni conto di questo segnale. Puoi farlo in ogni tool, oppure spostare parte della logica in un layer separato — il Gateway.

Una buona architettura di localizzazione deve rispondere a tre domande:

  1. Dove prendiamo la decisione su quale lingua e regione usare.
  2. Dove scegliamo i dati e le integrazioni necessari (cataloghi, API dei negozi, valute).
  3. Dove e come conserviamo lo stato dell’utente (locale, valuta, eventualmente alcune preferenze), per non doverlo passare ogni volta a mano.

Oggi vedremo proprio questo.

2. MCP, _meta e natura stateless: perché è necessario passare esplicitamente la locale

Prima di decidere dove esattamente nell’architettura considerare la locale, è utile ricordare come appare una richiesta MCP a livello di protocollo e quali metadati la piattaforma già trasmette.

Ricordo un fatto importante: le richieste MCP sono messaggi JSON‑RPC. Ogni messaggio è autonomo, il protocollo non impone una sessione stateful. Pertanto, se vuoi che il server consideri la locale, devi o:

  • passarla esplicitamente come argomento del tool (locale in inputSchema), oppure
  • leggerla da _meta["openai/locale"], che ChatGPT aggiunge alla richiesta.

Il più semplice esempio di handler che legge la locale da _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;

    // Usiamo quindi locale e country per scegliere il catalogo
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

Qui non passiamo la locale tramite gli argomenti, ma ci basiamo su _meta, che l’SDK ha già messo dentro extra. È una soluzione perfettamente valida, utile nel primo modello — con un unico MCP multilingue.

Nel secondo modello — con Gateway — anche _meta gioca un ruolo chiave: il gateway legge la locale dai metadati e, in base a ciò, decide dove inoltrare la richiesta. In quale forma memorizzare la locale — solo in _meta o anche negli schemi dei tool — lo vedremo in un blocco separato più avanti.

3. Modello 1: un unico server MCP multilingue («monolite poliglotta»)

Iniziamo dall’opzione architetturale più semplice. Hai un solo server MCP, un solo URL, un solo deploy, una sola codebase. All’interno di ogni tool:

  1. Ottieni la locale (da _meta o da un argomento).
  2. In base alla locale scegli le risorse necessarie: gift_catalog.en.json, gift_catalog.ru.json e così via.
  3. Restituisci il risultato già nella lingua richiesta.

Esempio per GiftGenius

Supponiamo di avere due file di catalogo:

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

Creiamo un piccolo helper loadGiftCatalog(locale), che seleziona il file giusto:

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 di regali
}

Ora il nostro tool suggest_gifts può semplicemente chiamare questo helper:

server.registerTool(
  "suggest_gifts",
  { title: "Selezione di regali", 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 } };
  }
);

Ne risulta che la localizzazione è nascosta in un unico punto — in loadGiftCatalog, e i tool passano semplicemente la locale a quella funzione. Allo stesso modo si possono scegliere i formati di data, le valute e qualsiasi altra caratteristica dipendente dalla regione.

Pro e contro di questo modello

Per non perdersi nel testo, riassumiamo i pro e contro di questo primo modello in una piccola tabella (solo sul «singolo MCP» — il confronto con il Gateway arriverà più avanti).

Criterio Un unico MCP multilingue
Numero di istanze MCP 1
Dove si considera la locale Nel codice dei tool
Deployment e scalabilità Più semplice, un solo punto
Localizzazione dei cataloghi Tramite caricamento condizionale di file/richieste
Quantità di codice if (locale ...) Diventa tanta
Supporto di mercati/API diversi Tutto il «parco» in un unico codice

Questo modello è perfetto per:

  • MVP e piccole app, con 2–3 lingue e mercati non troppo differenti;
  • progetti didattici (ad esempio il nostro GiftGenius nel corso).

È meno adatto quando:

  • le lingue diventano molte,
  • team e dati per i diversi mercati sono sostanzialmente differenti (DB separate, API e‑commerce dedicate, requisiti legali propri).

Ed è proprio in questi casi che entra in scena il secondo modello.

4. Modello 2: MCP Gateway + server backend monolingue

Immaginiamo ora che GiftGenius funzioni negli USA, in Russia e, poniamo, in Germania. Per gli USA usi Amazon API, per la Russia Ozon, per la Germania un retailer locale. Ogni mercato ha il proprio contratto, le proprie peculiarità, il proprio team. Mettere tutto in un unico monolite MCP è scomodo.

L’idea del modello 2 è questa:

Tra ChatGPT e i servizi MCP reali c’è un Gateway. Per ChatGPT è semplicemente un altro server MCP, ma internamente instrada le richieste verso diversi server backend, ciascuno dei quali «parla» una sola lingua e lavora con un singolo mercato.

Come appare nel diagramma

Disegniamo prima il confronto fra i due modelli.

flowchart LR
    subgraph Model1["Modello 1: Un MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (multilingue)"]
    end

    subgraph Model2["Modello 2: Gateway + monolingui"]
      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, Local shop)"]
    end

Agli occhi di ChatGPT nel secondo modello esiste un solo endpoint MCP — il Gateway. Internamente analizza _meta["openai/locale"] e/o _meta["openai/userLocation"] e sceglie il backend corretto.

Cosa fa il Gateway (nel contesto di questa lezione)

È importante non trasformare il Gateway in «un secondo monolite con tutta la business logic». Nel nostro modulo il suo ruolo è fortemente limitato:

  1. Ricevere il messaggio MCP da ChatGPT (incluso _meta).
  2. Estrarre locale / userLocation.
  3. In base a ciò, scegliere il server backend necessario.
  4. Proxare la richiesta (JSON‑RPC) e restituire la risposta.

Tutte le decisioni su quale catalogo di regali usare, come chiamare Amazon o Ozon, restano dentro il singolo server MCP linguistico. Il Gateway non sa cosa sia «il regalo perfetto per la suocera». Gli basta sapere che per ru-RU deve andare su mcp-giftgenius-ru, e per en-US su mcp-giftgenius-en.

Uno scheletro minimo di MCP Gateway in TypeScript

Semplifichiamo molto, per non perdersi nei dettagli. Immaginiamo di avere un helper callDownstreamTool che sa parlare con i server MCP interni via JSON‑RPC (potrebbero essere richieste HTTP o una connessione SSE persistente, ma i dettagli li lasciamo al modulo 16).

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

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

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // default
  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"
    // Chiamiamo lo stesso tool sul server backend appropriato
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

I server MCP interni registrano suggest_gifts con esattamente lo stesso contratto, ma ognuno lavora solo con la propria lingua/mercato e non sa che esistono altre lingue.

Allo stesso modo il Gateway può proxyare listTools, listResources e altri metodi MCP, ma questo è già tema di un altro modulo.

5. Confronto dei due modelli per la localizzazione

Prima abbiamo visto pro e contro del modello «un solo MCP». Ora riassumiamo le differenze di entrambi i modelli secondo i parametri principali.

Criterio Un unico MCP multilingue Gateway + server MCP monolingue
Numero di servizi MCP 1 1 Gateway + N server backend
Dove si considera la locale Dentro ogni tool (logica if locale ...) Nel Gateway, che instrada; all’interno dei servizi la lingua è fissa
Flessibilità UX (cambio lingua) Facile, tutto in un posto, il LLM cambia semplicemente la locale Possibile, ma va pensato come il Gateway effettui lo switch del backend
Complessità dell’infrastruttura Minima Maggiore: servono deploy separati per ogni lingua
Isolamento per mercati Basso: un solo codice, un solo processo Alto: il down del server RU non rompe EN e viceversa
Supporto di team diversi Più difficile dividere le responsabilità Naturale: i team RU, EN, DE possono sviluppare i propri MCP separatamente
Logica di localizzazione nel codice Mischiata con la business logic in ogni handler Concentrata nel Gateway e nei confini del singolo servizio backend

Per il nostro corso didattico ci atterremo per lo più al modello 1 (un solo MCP + locale come parametro), mentre il modello con Gateway sarà considerato come percorso naturale di scalabilità quando hai un «vero business» con decine di mercati. Detto ciò, poiché il Gateway è il passo successivo naturale, vedremo un dettaglio importante di questa architettura: come conservare locale e paese dell’utente nello stato di sessione.

6. Locale come parte dello stato del client nel Gateway

Finora abbiamo presupposto che ogni richiesta contenga tutto il necessario. Ma nella vita reale è comodo mantenere alcune informazioni nello stato della sessione. Ad esempio:

  • l’utente è arrivato una volta con locale = "ru-RU" e userLocation.country = "RU";
  • in seguito vuoi instradare tutte le sue richieste al backend RU, anche se alcune chiamate intermedie arrivano senza una locale esplicita negli argomenti.

MCP ha un campo utile _meta["openai/subject"] — un identificatore anonimo dell’utente che OpenAI invia ai tuoi servizi. Lo puoi usare come chiave di sessione.

Una semplice implementazione dello stato in memoria

Scriviamo un piccolo strato di state nel Gateway (ovviamente, in produzione al posto di Map è meglio usare Redis o un altro archivio esterno).

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

Ora nell’handler del Gateway si può prima aggiornare lo stato e poi usarlo per scegliere il server 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);
  }
);

In questo modo, «memorizzi» una volta la mappatura clientIdlocale, country e poi puoi usarla in tutte le chiamate successive ai tool, senza copiare i campi in ogni argomento.

Allo stesso modo il Gateway può ricordare la valuta preferita, il formato dei prezzi o altre impostazioni utili alla logica di commerce (ma di questo parleremo soprattutto nel modulo su ACP).

7. GiftGenius: due scenari e l’impatto della scelta architetturale

Per evitare l’impressione di parlare solo di quadratini astratti, vediamo scenari concreti di GiftGenius.

Scenario 1: Utente dalla Russia, scrive in russo

Supponiamo:

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

L’utente scrive: «Scegli un regalo per un collega, ama i giochi da tavolo, fino a 3000 rubli».

Nel modello 1 (un solo MCP):

  1. L’handler legge la locale da _meta, ottiene "ru-RU".
  2. Carica gift_catalog.ru.json, dove tutti i nomi sono in russo e i prezzi in rubli.
  3. Filtra per categoria e budget, restituisce un elenco strutturato di regali in russo.

Nel modello 2 (Gateway + monolingui):

  1. Il Gateway legge locale e userLocation, decide che è un utente RU.
  2. Inoltra la chiamata suggest_gifts a mcp-giftgenius-ru.
  3. Quest’ultimo lavora solo con il catalogo russo e l’API di Ozon, e restituisce i regali in rubli.

In entrambi i casi l’utente vede tutto nella propria lingua, ma nel secondo scenario il tuo server MCP inglese non è nemmeno a conoscenza dell’esistenza del catalogo per la Russia.

Scenario 2: Utente dalla Germania, scrive in inglese

Ora:

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

L’utente scrive: «Gift for my German coworker, budget 50 EUR».

Nel modello 1:

  • la locale "en" determina i testi in inglese,
  • e la country "DE" può essere usata per scegliere il catalogo con prezzi in euro e assortimento adattato all’Europa.

Nel modello 2:

  • Il Gateway può decidere che locale = "en" → servizio in inglese, ma country = "DE" → prodotti da magazzino europeo; a seconda della tua business logic puoi:
  • o inoltrare la richiesta a mcp-giftgenius-en con parametro country=DE,
  • oppure avere un mcp-giftgenius-eu separato per l’Europa.

Qui si vede bene che locale (lingua) e regione (userLocation) sono dimensioni diverse, e il Gateway è un luogo comodo per comporle nella decisione «quale servizio chiamare e quali prodotti mostrare».

8. Locale negli schemi dei tool vs locale solo in _meta

Indipendentemente dal fatto che tu usi un solo MCP o una combinazione Gateway + servizi monolingue, vale la pena discutere un punto sottile ma importante: conservare la locale solo in _meta o farne un argomento del tool?

Ci sono due approcci.

Primo: affidarsi solo a _meta.

È comodo perché gli schemi dei tool non si appesantiscono con un altro campo. Il server legge la locale da extra._meta e decide autonomamente. Nel modello 1 spesso è sufficiente.

Secondo: aggiungere esplicitamente locale (e, eventualmente, currency) nell’inputSchema del tool.

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

Poi, nel system prompt, puoi chiedere al modello di compilare sempre l’argomento locale usando il valore dal contesto dell’utente. Questo rende le intenzioni trasparenti: negli argomenti JSON si vede chiaramente in quale lingua il server debba lavorare. Tale approccio è particolarmente utile in un’architettura più complessa, dove c’è un MCP comune che, in base a locale, instrada internamente verso servizi o risorse diversi.

Nella pratica spesso si combinano entrambi gli approcci: negli schemi è presente il campo locale, ma se per qualche motivo il modello non lo compila, il server si copre leggendo _meta["openai/locale"].

9. Dove passa il confine tra localizzazione e «logica in eccesso» nel Gateway

La trappola in cui è facile cadere: visto che abbiamo un Gateway «intelligente», facciamogli:

  • decidere da solo quali regali mostrare,
  • formattare da solo date e prezzi,
  • assemblare da solo report sui clic e così via.

Sembra allettante, ma trasforma il Gateway in «un secondo monolite» e ne complica aggiornamento ed esercizio. Nelle pratiche industriali dei gateway API (e l’MCP Gateway, per ruolo, è un gateway) il focus resta su pochi compiti: autenticazione, autorizzazione, instradamento e leggero arricchimento del contesto. Ad esempio, il gateway può convertire header HTTP in metadati comodi. La business logic e le operazioni pesanti devono vivere nei servizi backend.

Per la localizzazione questo significa:

  • Il Gateway può fare il parsing di _meta["openai/locale"] e _meta["openai/userLocation"].
  • Può memorizzarli nello stato del client.
  • Può scegliere il server linguistico giusto o aggiungere al request il campo locale/country.

Ma la selezione dei regali, il filtraggio per età, budget, ecc. — tutto questo deve restare nei backend MCP.

10. Errori tipici nella progettazione della localizzazione tramite MCP e Gateway

Errore n. 1: Affidarsi solo all’«indovinare» la lingua dal testo dell’utente.
A volte viene voglia di prendere il testo del messaggio, farlo passare da un language detector e, sulla base di quello, decidere quale server chiamare. Può essere un fallback utile, ma non il meccanismo principale. La piattaforma ti fornisce già openai/locale e openai/userLocation, che tengono conto delle impostazioni di ChatGPT e dell’ambiente dell’utente. Ignorare questi segnali e giocare a «indovina la lingua» è un modo efficace per rompere la UX nei modi più inaspettati.

Errore n. 2: Conservare la locale solo «nella testa del modello» e non passarla al server.
Se locale non compare né in _meta né negli argomenti del tool, il server non sa nulla della lingua dell’utente. Il modello può provare a tradurre la stringa «книги» in books, ma non è affidabile, soprattutto con categorie complesse. La via giusta è passare esplicitamente la locale: o tramite l’argomento locale, oppure leggendola da _meta e costruendo l’architettura attorno a ciò.

Errore n. 3: Spostare tutta la business logic di localizzazione nel Gateway.
Se il Gateway inizia a scegliere i regali, a interrogare database e a combattere con API esterne, smette di essere un router leggero e diventa un servizio pesante, difficile da scalare e aggiornare. Di fatto ottieni due monoliti invece di uno. Meglio mantenere il Gateway il più «semplice» possibile: guarda locale/userLocation, sceglie il backend giusto e inoltra con cura i metadati.

Errore n. 4: Vincolare il routing solo all’IP o a userLocation.
A volte viene voglia di fare semplice: «se il paese è RU — vai al server RU». Ma l’utente può trovarsi in Germania e volere comunque un’interfaccia in russo, oppure può chiedere «switch to English» a metà sessione. Se nel Gateway non consideri openai/locale e la possibile volontà dell’utente di cambiare lingua, il routing diventa «di cemento» e rovina la UX. Meglio basarsi su una combinazione di locale e userLocation, e mantenere la possibilità di override delle impostazioni tramite lo stato di sessione.

Errore n. 5: Non usare _meta["openai/subject"] e duplicare tutti i parametri in ogni argomento.
Quando in ogni argomento del tool inizi a trascinare locale, country, currency, userId e mezzo interfaccia, la vita diventa presto triste. MCP già trasmette un identificatore anonimo dell’utente tramite _meta["openai/subject"], e puoi conservare tutte queste informazioni nello stato del client lato Gateway o server backend. Questo semplificherà i contratti e ridurrà il rischio di desincronizzazione degli argomenti.

Errore n. 6: Nessuna strategia evolutiva: «costruiamo subito un Gateway complesso per dieci lingue».
Spesso si vuole fare tutto perfetto da subito: Gateway, cinque lingue, tre regioni, dieci servizi MCP. In pratica è più semplice iniziare dal modello «un solo MCP + parametro locale o _meta», portare il comportamento a stabilità, e poi estrarre Gateway e servizi monolingue con la crescita. Tentare di costruire subito un enorme «zoo» quasi certamente ritarderà il rilascio e complicherà il debug.

1
Sondaggio/quiz
Localizzazione, livello 9, lezione 4
Non disponibile
Localizzazione
Localizzazione (UI, dati, descrizioni delle funzioni)
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION