CodeGym /Cours /ChatGPT Apps /MCP Gateway et architecture de localisation : serveurs mo...

MCP Gateway et architecture de localisation : serveurs monolingues, locale comme paramètre, état client

ChatGPT Apps
Niveau 9 , Leçon 4
Disponible

1. Pourquoi se préoccuper de l’architecture de la localisation

Tant que vous n’avez qu’une langue et un petit catalogue, c’est simple : vous stockez gift_catalog.json, tous les textes en russe, et le serveur MCP sert honnêtement ces cadeaux à tout le monde. Mais dès que vous voulez :

  • une interface anglaise pour les États‑Unis et l’Europe,
  • un catalogue distinct en russe avec des matriochkas et des livres en russe,
  • des marchés différents (Amazon pour les US, Ozon pour la Russie),

l’approche naïve « ajouter dans chaque handler un if (locale === "ru") » commence à transformer le code en sapin de Noël.

MCP est à la fois un protocole et une implémentation serveur de ce protocole. Le serveur reçoit des requêtes de ChatGPT avec des métadonnées, dont locale et userLocation. La question n’est pas de savoir « s’il peut lire locale », mais où exactement dans l’architecture vous tenez compte de ce signal. On peut le faire dans chaque outil, ou bien externaliser une partie de la logique dans une couche dédiée — le Gateway.

Une bonne architecture de localisation doit répondre à trois questions :

  1. Où prenons‑nous la décision de la langue et de la région à utiliser.
  2. Où choisissons‑nous les données et intégrations nécessaires (catalogues, API de boutiques, devises).
  3. Où et comment stockons‑nous l’état utilisateur (locale, devise, éventuellement certaines préférences), pour ne pas le transmettre à la main à chaque fois.

C’est exactement ce que nous allons examiner aujourd’hui.

2. MCP, _meta et nature stateless : pourquoi il faut transmettre locale explicitement

Avant de décider où prendre en compte locale dans l’architecture, il est utile de se rappeler à quoi ressemble une requête MCP au niveau du protocole et quelles métadonnées la plateforme transmet déjà.

Fait important : les requêtes MCP sont des messages JSON‑RPC. Chaque message est autonome, le protocole ne vous impose pas une session stateful. Par conséquent, si vous voulez que le serveur prenne en compte la locale, il faut soit :

  • la transmettre explicitement comme argument d’outil (locale dans le inputSchema), soit
  • la lire depuis _meta["openai/locale"], que ChatGPT ajoute à la requête.

Un exemple minimal de handler qui lit locale depuis _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;

    // On utilise ensuite locale et country pour choisir le catalogue
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

Ici, nous ne faisons pas passer locale via les arguments, nous nous appuyons sur _meta, que le SDK a déjà placé dans extra. C’est une option tout à fait valable, utile dans le premier modèle — avec un MCP multilingue unique.

Dans le deuxième modèle — avec un Gateway — _meta joue aussi un rôle clé : le relais lit locale dans les métadonnées et décide à quel service envoyer la requête. Sous quelle forme stocker locale — uniquement dans _meta ou aussi dans les schémas des outils — nous le verrons plus bas.

3. Modèle 1 : un seul serveur MCP multilingue (« monolithe polyglotte »)

Commençons par l’option la plus simple. Vous avez un seul serveur MCP, une seule URL, un seul déploiement, une seule base de code. À l’intérieur de chaque outil, vous :

  1. Récupérez locale (depuis _meta ou un argument).
  2. Choisissez les ressources nécessaires selon locale : gift_catalog.en.json, gift_catalog.ru.json, etc.
  3. Retournez le résultat déjà dans la bonne langue.

Exemple pour GiftGenius

Supposons que nous ayons deux fichiers de catalogues :

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

Créons un petit helper loadGiftCatalog(locale) qui choisit le bon fichier :

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; // tableau de cadeaux
}

Notre outil suggest_gifts peut maintenant simplement appeler ce helper :

server.registerTool(
  "suggest_gifts",
  { title: "Sélection de cadeaux", 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 } };
  }
);

Ainsi, la localisation est concentrée en un seul endroit — loadGiftCatalog — et les outils se contentent d’y passer locale. De la même manière, on peut choisir les formats de dates, devises et autres éléments dépendant de la région.

Avantages et inconvénients de ce modèle

Pour ne pas nous noyer dans le texte, résumons les avantages et inconvénients de ce premier modèle (uniquement « un MCP » — la comparaison avec le Gateway viendra ensuite).

Critère Un MCP multilingue
Nombre d’instances MCP 1
Où locale est prise en compte Dans le code des outils
Déploiement et mise à l’échelle Plus simple, un seul point
Localisation des catalogues Via un chargement conditionnel de fichiers/requêtes
Code if (locale ...) Prolifère
Prise en charge de différents marchés/API Tout le « zoo » dans un seul code

Ce modèle convient très bien pour :

  • un MVP et de petites applications avec 2–3 langues et des marchés peu différents ;
  • des projets d’apprentissage (par exemple, notre GiftGenius dans le cadre du cours).

Il convient moins lorsque :

  • le nombre de langues augmente,
  • les équipes et données diffèrent fondamentalement selon les marchés (BD séparées, API e‑commerce propres, exigences légales spécifiques).

Et c’est justement dans ces cas que le deuxième modèle entre en scène.

4. Modèle 2 : MCP Gateway + serveurs backend monolingues

Imaginons maintenant que GiftGenius fonctionne aux US, en Russie et, disons, en Allemagne. Pour les US, vous utilisez l’API Amazon, pour la Russie — Ozon, pour l’Allemagne — un détaillant local. Chaque marché a son contrat, ses spécificités, son équipe. Tout mettre dans un seul monolithe MCP n’est pas agréable.

L’idée du modèle 2 est la suivante :

Entre ChatGPT et les services MCP réels, on place un Gateway. Pour ChatGPT, c’est juste un autre serveur MCP, et en interne il achemine les requêtes vers différents serveurs backend, chacun « parlant » une seule langue et opérant sur un seul marché.

À quoi cela ressemble sur un diagramme

Commençons par comparer les deux modèles.

flowchart LR
    subgraph Model1["Modèle 1 : un MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (multilingue)"]
    end

    subgraph Model2["Modèle 2 : Gateway + monolingues"]
      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

Aux yeux de ChatGPT, dans le deuxième modèle, il n’existe qu’un seul endpoint MCP — le Gateway. En interne, il analyse _meta["openai/locale"] et/ou _meta["openai/userLocation"] et choisit le backend approprié.

Ce que fait le Gateway (dans le contexte de ce cours)

Important : n’en faites pas « un deuxième monolithe avec toute la logique métier ». Dans notre module, son rôle est fortement limité :

  1. Recevoir le message MCP de ChatGPT (y compris _meta).
  2. Extraire locale / userLocation.
  3. Choisir le serveur backend adéquat sur cette base.
  4. Proxifier la requête (JSON‑RPC) et renvoyer la réponse.

Toutes les décisions sur le choix du catalogue de cadeaux, la manière d’appeler Amazon ou Ozon restent au sein de chaque serveur MCP linguistique. Le Gateway ne sait pas à quoi ressemble « le cadeau idéal pour la belle‑mère ». Il lui suffit de savoir que pour ru-RU il faut appeler mcp-giftgenius-ru, et pour en-USmcp-giftgenius-en.

Un squelette minimal de MCP Gateway en TypeScript

Simplifions fortement pour ne pas noyer dans les détails. Imaginons que nous ayons un helper callDownstreamTool qui sait parler aux serveurs MCP internes en JSON‑RPC (ce pourraient être des requêtes HTTP ou une connexion SSE persistante, mais nous laisserons les détails au module 16).

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

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

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // valeur par défaut
  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"
    // Appeler le même outil sur le backend approprié
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

Les serveurs MCP internes enregistrent suggest_gifts avec exactement le même contrat, mais chacun ne travaille que dans sa langue/marché et ignore l’existence des autres langues.

De la même manière, le Gateway peut proxifier listTools, listResources et d’autres méthodes MCP, mais cela relève d’un autre module.

5. Comparaison des deux modèles pour la localisation

Nous avons déjà étudié séparément les avantages et inconvénients du modèle « un seul MCP ». Rassemblons maintenant les différences entre les deux modèles sur les paramètres principaux.

Critère Un MCP multilingue Gateway + serveurs MCP monolingues
Nombre de services MCP 1 1 Gateway + N serveurs backend
Où locale est prise en compte À l’intérieur de chaque outil (logique if locale ...) Dans le Gateway qui fait le routage ; à l’intérieur des services la langue est fixe
Flexibilité UX (changement de langue) Facile, tout est au même endroit, le LLM change simplement locale Possible, mais il faut prévoir comment le Gateway basculera de backend
Complexité de l’infrastructure Minimale Plus élevée : déploiements séparés pour chaque langue
Isolation par marchés Faible : un code, un processus Élevée : la panne du serveur RU n’affecte pas EN et inversement
Support d’équipes distinctes Plus difficile de répartir les responsabilités Naturel : les équipes RU, EN, DE peuvent développer leurs MCP séparément
Logique de localisation dans le code Mélangée à la logique métier dans chaque handler Concentrée dans le Gateway et aux frontières de chaque service backend

Dans notre cours, nous suivrons principalement le modèle 1 (un MCP + locale comme paramètre), et nous considérerons le modèle avec Gateway comme une voie naturelle de montée en charge lorsque vous avez déjà « un vrai business » avec des dizaines de marchés. Néanmoins, puisque le Gateway est l’étape suivante logique, examinons un détail important de cette architecture : comment stocker locale et le pays de l’utilisateur dans l’état de session.

6. Locale comme partie de l’état client dans le Gateway

Jusqu’ici, nous avons supposé que chaque requête contenait tout ce qu’il faut. Mais dans la vraie vie, il est pratique de conserver une partie des informations dans l’état de session. Par exemple :

  • l’utilisateur arrive une fois avec locale = "ru-RU" et userLocation.country = "RU" ;
  • ensuite, vous souhaitez router toutes ses requêtes vers le backend RU, même si certains appels intermédiaires arrivent sans locale explicite dans les arguments.

MCP dispose d’un champ utile _meta["openai/subject"] — un identifiant utilisateur anonyme que OpenAI envoie à vos services. Vous pouvez l’utiliser comme clé de session.

Implémentation simple de l’état en mémoire

Écrivons une fine couche d’état dans le Gateway (bien sûr, en production, au lieu de Map il vaut mieux utiliser Redis ou un autre stockage externe).

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

Désormais, dans le handler du Gateway, on peut d’abord mettre à jour l’état, puis l’utiliser pour choisir le serveur 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);
  }
);

Ainsi, vous « mémorisez » une fois l’association clientIdlocale, country et vous pouvez ensuite l’utiliser dans tous les appels d’outils suivants, sans recopier ces champs dans chaque argument.

De même, le Gateway peut se souvenir de la devise préférée, du format des prix ou d’autres réglages utiles à la logique commerce (mais cela relève plutôt du module ACP).

7. GiftGenius : deux scénarios et l’impact du choix d’architecture

Pour éviter l’impression de parler d’abstractions, regardons des scénarios concrets de GiftGenius.

Scénario 1 : Utilisateur en Russie, écrit en russe

Supposons que nous ayons :

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

L’utilisateur écrit : « Trouve un cadeau pour un collègue, il aime les jeux de société, jusqu’à 3 000 roubles ».

Dans le modèle 1 (un seul MCP) :

  1. Le handler lit locale depuis _meta, obtient "ru-RU".
  2. Charge gift_catalog.ru.json, où tous les intitulés sont en russe et les prix en roubles.
  3. Filtre par catégorie et budget, et renvoie une liste structurée de cadeaux en russe.

Dans le modèle 2 (Gateway + monolingues) :

  1. Le Gateway lit locale et userLocation, décide que c’est un utilisateur RU.
  2. Redirige l’appel suggest_gifts vers mcp-giftgenius-ru.
  3. Celui‑ci ne travaille qu’avec le catalogue russe et l’API Ozon, et renvoie des cadeaux en roubles.

Dans les deux cas, l’utilisateur voit tout dans sa langue, mais dans le deuxième, votre serveur MCP anglais n’a même pas connaissance du catalogue pour la Russie.

Scénario 2 : Utilisateur en Allemagne, écrit en anglais

Maintenant :

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

L’utilisateur écrit : « Gift for my German coworker, budget 50 EUR ».

Dans le modèle 1 :

  • locale "en" donne des textes en anglais,
  • et country "DE" peut être utilisé pour choisir un catalogue avec des prix en euros et une offre adaptée à l’Europe.

Dans le modèle 2 :

  • Le Gateway peut décider que locale = "en" → service anglais, mais country = "DE" → produits depuis un entrepôt européen ; selon votre logique métier, vous pouvez :
  • soit diriger la requête vers mcp-giftgenius-en avec le paramètre country=DE,
  • soit disposer d’un mcp-giftgenius-eu séparé pour l’Europe.

On voit bien ici que la locale (langue) et la région (userLocation) sont deux dimensions différentes, et que le Gateway est un endroit pratique pour les combiner en une décision « quel service appeler et quels produits afficher ».

8. Locale dans les schémas d’outils vs locale uniquement dans _meta

Que vous utilisiez un seul MCP ou un duo Gateway + services monolingues, il reste à discuter un point subtil mais important : faut‑il garder locale uniquement dans _meta ou en faire aussi un argument d’outil ?

Il y a deux approches.

Première : s’appuyer uniquement sur _meta.

C’est pratique, car les schémas d’outils ne sont pas encombrés d’un champ de plus. Le serveur lit locale depuis extra._meta et décide. Dans le modèle 1, cela suffit souvent.

Deuxième : ajouter explicitement locale (et éventuellement currency) dans le inputSchema de l’outil.

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

Ensuite, dans le system‑prompt, vous pouvez demander au modèle de toujours renseigner locale comme argument, en utilisant la valeur du contexte utilisateur. Cela rend les intentions transparentes : dans les arguments JSON, on voit directement dans quelle langue le serveur doit travailler. Cette approche est particulièrement utile dans une architecture plus complexe, où il existe un MCP commun qui route selon locale vers différents services ou ressources.

En pratique, on combine souvent les deux : les schémas contiennent un champ locale, mais si, pour une raison quelconque, le modèle ne le remplit pas, le serveur se rabat sur _meta["openai/locale"].

9. Où se situe la frontière entre localisation et « logique en trop » dans le Gateway

Un piège facile : puisque nous avons un Gateway « intelligent », laissons‑le :

  • choisir lui‑même quels cadeaux afficher,
  • mettre lui‑même en forme les dates et les prix,
  • assembler lui‑même des rapports de clics, etc.

C’est tentant, mais cela transforme le Gateway en « deuxième monolithe » et complique sa mise à jour et son exploitation. Dans les pratiques industrielles des API gateways (et un MCP Gateway joue le même rôle), on se concentre sur quelques tâches : authentification, autorisation, routage et léger enrichissement de contexte. Par exemple, le relais peut transformer des en‑têtes HTTP en métadonnées pratiques. La logique métier et les opérations lourdes doivent vivre dans les services backend.

Pour la localisation, cela signifie :

  • Le Gateway peut parser _meta["openai/locale"] et _meta["openai/userLocation"].
  • Il peut les mémoriser dans l’état client.
  • Il peut choisir le serveur linguistique approprié ou ajouter au message un champ locale/country.

Mais la sélection des cadeaux, le filtrage par âge, budget, etc., doivent rester dans les backend MCP.

10. Erreurs courantes lors de la conception de la localisation avec MCP et Gateway

Erreur n°1 : s’appuyer uniquement sur la « détection » de la langue du texte utilisateur.
Parfois, on veut prendre le message, le passer dans un détecteur de langue et décider ainsi quel serveur appeler. Cela peut être un repli utile, mais pas le mécanisme principal. La plateforme vous fournit déjà openai/locale et openai/userLocation, qui tiennent compte des réglages ChatGPT et de l’environnement utilisateur. Ignorer ces signaux et « deviner la langue » est un moyen efficace de casser l’UX dans les cas les plus inattendus.

Erreur n°2 : garder locale uniquement « dans la tête » du modèle sans la transmettre au serveur.
Si locale n’apparaît ni dans _meta ni dans les arguments de l’outil, le serveur ne sait rien de la langue de l’utilisateur. Le modèle peut bien sûr tenter de traduire la chaîne « книги » en books, mais c’est peu fiable, surtout si vos catégories sont complexes. La bonne approche consiste à transmettre locale explicitement : soit via un argument locale, soit en la lisant depuis _meta et en construisant l’architecture autour de cela.

Erreur n°3 : déplacer toute la logique métier de localisation dans le Gateway.
Si le Gateway commence à choisir les cadeaux, appeler les bases de données et combattre avec des API externes, il cesse d’être un routeur léger et devient un service lourd, difficile à faire évoluer et à mettre à jour. Vous obtenez deux monolithes au lieu d’un. Il vaut mieux garder le Gateway aussi « bête » que possible : il regarde locale/userLocation, choisit le backend approprié et transmet proprement les métadonnées.

Erreur n°4 : lier le routage strictement à l’IP ou à userLocation.
Parfois, on veut faire simple : « si le pays est RU — aller sur le serveur RU ». Mais un utilisateur peut se trouver en Allemagne et vouloir une interface en russe, ou demander « switch to English » au milieu d’une session. Si votre Gateway ne prend pas en compte openai/locale et la possibilité de changer de langue, le routage devient « en béton » et casse l’UX. Mieux vaut s’appuyer sur une combinaison de locale et userLocation, tout en gardant la possibilité de surcharger via l’état de session.

Erreur n°5 : ne pas utiliser _meta["openai/subject"] et dupliquer tous les paramètres dans chaque argument.
Lorsque vous commencez à transporter dans chaque argument d’outil locale, country, currency, userId et la moitié de l’interface, la vie devient vite pénible. MCP transmet déjà un identifiant utilisateur anonyme via _meta["openai/subject"], et vous pouvez stocker toutes ces informations dans l’état client côté Gateway ou backend. Cela simplifiera les contrats et réduira les risques d’incohérence des arguments.

Erreur n°6 : pas de stratégie d’évolution : « construire tout de suite un Gateway complexe pour dix langues ».
On veut souvent faire parfait dès le départ : Gateway, cinq langues, trois régions, dix services MCP. En pratique, il est plus simple de commencer par le modèle « un MCP + paramètre locale ou _meta », stabiliser le comportement, puis sortir un Gateway et des services monolingues au fur et à mesure de la croissance. Tenter de bâtir tout de suite un immense « zoo » retarde presque à coup sûr la sortie et complique le débogage.

1
Étude/Quiz
Localisation, niveau 9, leçon 4
Indisponible
Localisation
Localisation (UI, données, descriptions de fonctions)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION