1. Warum überhaupt über Lokalisierungsarchitektur nachdenken
Solange Sie eine Sprache und einen kleinen Katalog haben, ist alles einfach: Sie halten gift_catalog.json vor, alle Texte sind auf Russisch, und der MCP‑Server liefert diese Geschenke brav an alle aus. Aber sobald Sie wollen:
- eine englische UI für die USA und Europa,
- einen separaten russischsprachigen Katalog mit Matrjoschkas und Büchern auf Russisch,
- verschiedene Märkte (Amazon für die USA, Ozon für Russland),
beginnt der naive Ansatz „in jedem Handler noch ein if (locale === "ru")“ den Code in einen Weihnachtsbaum zu verwandeln.
MCP ist einerseits ein Protokoll und andererseits eine Serverimplementierung dieses Protokolls. Der Server erhält Anfragen von ChatGPT mit Metadaten, darunter locale und userLocation. Die Frage ist nicht, „ob er locale lesen kann“, sondern wo in der Architektur Sie dieses Signal berücksichtigen. Man kann es in jedem Tool tun oder einen Teil der Logik in eine separate Schicht — den Gateway — auslagern.
Eine gute Lokalisierungsarchitektur sollte drei Fragen beantworten:
- Wo treffen wir die Entscheidung, welche Sprache und Region verwendet werden soll.
- Wo wählen wir die nötigen Daten und Integrationen aus (Kataloge, Shop‑APIs, Währungen).
- Wo und wie speichern wir den Benutzerzustand (locale, Währung, ggf. Präferenzen), damit wir das nicht jedes Mal manuell übergeben müssen.
Genau das schauen wir uns heute an.
2. MCP, _meta und stateless‑Natur: warum locale explizit übergeben werden muss
Bevor wir entscheiden, wo genau in der Architektur locale berücksichtigt wird, ist es hilfreich, sich in Erinnerung zu rufen, wie eine MCP‑Anfrage auf Protokollebene aussieht und welche Metadaten die Plattform bereits übermittelt.
Ein wichtiger Fakt: MCP‑Anfragen sind JSON‑RPC‑Nachrichten. Jede Nachricht steht für sich; das Protokoll erzwingt keine stateful Session. Wenn Sie also möchten, dass der Server die Lokale berücksichtigt, muss man sie entweder:
- explizit als Tool‑Argument übergeben (locale im inputSchema), oder
- aus _meta["openai/locale"] lesen, das ChatGPT der Anfrage hinzufügt.
Ein einfaches Beispiel eines Handlers, der locale aus _meta liest:
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;
// Danach verwenden wir locale und country zur Katalogauswahl
const gifts = await loadGiftCatalog(locale, country);
return { structuredContent: { gifts } };
}
);
Hier schleusen wir locale nicht über die Argumente durch, sondern verlassen uns auf _meta, das das SDK bereits in extra abgelegt hat. Das ist völlig praktikabel und wird uns im ersten Modell — mit einem mehrsprachigen MCP — nützlich sein.
Im zweiten Modell — mit Gateway — spielt _meta ebenfalls eine Schlüsselrolle: Der Gateway liest locale aus den Metadaten und entscheidet auf dieser Basis, wohin die Anfrage weitergeleitet wird. In welcher Form locale genau gehalten wird — nur in _meta oder zusätzlich in den Tool‑Schemas — besprechen wir weiter unten separat.
3. Modell 1: ein mehrsprachiger MCP‑Server („Polyglott‑Monolith“)
Beginnen wir mit der einfachsten Architekturvariante. Sie haben einen MCP‑Server, eine URL, ein Deployment, eine Codebasis. Innerhalb jedes Tools:
- Holen Sie sich die locale (aus _meta oder als Argument).
- Wählen Sie basierend auf der locale die benötigten Ressourcen: gift_catalog.en.json, gift_catalog.ru.json usw.
- Geben Sie das Ergebnis bereits in der benötigten Sprache zurück.
Beispiel für GiftGenius
Angenommen, wir haben zwei Katalogdateien:
- data/gift_catalog.en.json
- data/gift_catalog.ru.json
Wir schreiben einen kleinen Helper loadGiftCatalog(locale), der die passende Datei auswählt:
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 von Geschenken
}
Nun kann unser Tool suggest_gifts diesen Helper einfach aufrufen:
server.registerTool(
"suggest_gifts",
{ title: "Geschenkeauswahl", 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 } };
}
);
So ist die Lokalisierung an einer Stelle gekapselt — in loadGiftCatalog — und die Tools geben einfach die locale dorthin weiter. Genauso kann man Datums‑, Währungsformate und beliebige andere regionsabhängige Dinge auswählen.
Vor- und Nachteile dieses Modells
Um den Text übersichtlich zu halten, fassen wir die Vor‑ und Nachteile des ersten Modells kurz in einer Tabelle zusammen (nur „ein MCP“ — zum Vergleich mit dem Gateway kommen wir später zurück).
| Kriterium | Ein mehrsprachiger MCP |
|---|---|
| Anzahl der MCP‑Instanzen | 1 |
| Wo locale berücksichtigt wird | Im Code der Tools |
| Deployment und Skalierung | Einfacher, eine Stelle |
| Lokalisierung der Kataloge | Über bedingtes Laden von Dateien/Requests |
| Code if (locale ...) | Wird viel |
| Unterstützung verschiedener Märkte/APIs | Der ganze „Zoo“ in einem Code |
Dieses Modell eignet sich gut für:
- MVPs und kleine Apps mit 2–3 Sprachen und nicht allzu unterschiedlichen Märkten;
- Lehrprojekte (z. B. unser GiftGenius im Rahmen des Kurses).
Es eignet sich schlechter, wenn:
- die Anzahl der Sprachen stark wächst,
- Teams und Daten für verschiedene Märkte grundsätzlich verschieden sind (separate DBs, eigene E‑Commerce‑APIs, eigene rechtliche Anforderungen).
Genau in solchen Fällen kommt das zweite Modell ins Spiel.
4. Modell 2: MCP Gateway + einsprachige Backend‑Server
Stellen wir uns vor, GiftGenius läuft in den USA, in Russland und, sagen wir, in Deutschland. Für die USA nutzen Sie die Amazon‑API, für Russland — Ozon, für Deutschland — einen lokalen Händler. Jeder Markt hat seinen eigenen Vertrag, Besonderheiten und ein eigenes Team. Alles in einen MCP‑Monolithen zu stopfen ist unangenehm.
Die Idee von Modell 2 ist:
Zwischen ChatGPT und den eigentlichen MCP‑Services steht ein Gateway. Für ChatGPT ist es einfach ein weiterer MCP‑Server; intern routet er Anfragen an verschiedene Backend‑Server, von denen jeder nur in einer Sprache „spricht“ und mit einem Markt arbeitet.
So sieht das im Diagramm aus
Zuerst zeichnen wir den Vergleich der beiden Modelle.
flowchart LR
subgraph Model1["Modell 1: Ein MCP"]
A1[ChatGPT] --> B1["GiftGenius MCP (mehrsprachig)"]
end
subgraph Model2["Modell 2: Gateway + einsprachige Server"]
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, Lokaler Shop)"]
end
Aus Sicht von ChatGPT gibt es im zweiten Modell nur einen MCP‑Endpoint — den Gateway. Intern analysiert er _meta["openai/locale"] und/oder _meta["openai/userLocation"] und wählt das richtige Backend.
Was das Gateway macht (im Kontext dieser Lektion)
Wichtig ist, das Gateway nicht in einen „zweiten Monolithen mit der ganzen Business‑Logik“ zu verwandeln. In unserem Modul ist seine Rolle stark begrenzt:
- Das MCP‑Message von ChatGPT annehmen (inklusive _meta).
- locale / userLocation extrahieren.
- Darauf basierend den benötigten Backend‑Server wählen.
- Die Anfrage dorthin (JSON‑RPC) proxyen und die Antwort zurückgeben.
Alle Entscheidungen, welchen Geschenkkatalog man nimmt, wie genau man Amazon oder Ozon aufruft, bleiben innerhalb des jeweiligen sprachspezifischen MCP‑Servers. Das Gateway weiß nicht, wie das „ideale Geschenk für die Schwiegermutter“ aussieht. Es reicht zu wissen, dass man für ru-RU zu mcp-giftgenius-ru geht und für en-US zu mcp-giftgenius-en.
Einfachstes Gerüst eines MCP‑Gateways in TypeScript
Wir vereinfachen stark, um nicht in Details zu ertrinken. Nehmen wir an, wir haben einen Helper callDownstreamTool, der mit internen MCP‑Servern über JSON‑RPC sprechen kann (das könnten HTTP‑Requests oder eine dauerhafte SSE‑Verbindung sein, aber die Details überlassen wir Modul 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"
// Wir rufen dasselbe Tool auf dem passenden Backend‑Server auf
return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
}
);
Die internen MCP‑Server registrieren bei sich suggest_gifts mit exakt demselben Vertrag, aber jeder arbeitet nur mit seiner Sprache/seinem Markt und weiß nicht, dass es andere Sprachen gibt.
Genauso kann das Gateway listTools, listResources und andere MCP‑Methoden proxyen, aber das ist Thema eines anderen Moduls.
5. Vergleich der beiden Modelle für die Lokalisierung
Zuvor haben wir die Vor‑ und Nachteile des Modells „ein MCP“ separat betrachtet. Jetzt führen wir die Unterschiede beider Modelle entlang zentraler Parameter zusammen.
| Kriterium | Ein mehrsprachiger MCP | Gateway + einsprachige MCP‑Server |
|---|---|---|
| Anzahl der MCP‑Services | 1 | 1 Gateway + N Backend‑Server |
| Wo locale berücksichtigt wird | In jedem Tool (Logik if locale ...) | Im Gateway, das routet; innerhalb der Services ist die Sprache fix |
| UX‑Flexibilität (Sprachwechsel) | Einfach, alles an einem Ort, das LLM ändert einfach die locale | Möglich, aber man muss durchdenken, wie das Gateway das Backend umschaltet |
| Infrastrukturkomplexität | Minimal | Höher: separate Deployments für jede Sprache nötig |
| Isolation nach Märkten | Niedrig: ein Code, ein Prozess | Hoch: Ausfall des RU‑Servers bricht EN nicht und umgekehrt |
| Unterstützung getrennter Teams | Schwieriger, Verantwortung zu trennen | Natürlich: Teams RU, EN, DE können ihre MCPs getrennt entwickeln |
| Lokalisierungslogik im Code | Mit der Business‑Logik in jedem Handler vermischt | Im Gateway und in den Grenzen des jeweiligen Backend‑Services konzentriert |
Für unseren Kurs halten wir uns überwiegend an Modell 1 (ein MCP + locale als Parameter), und das Gateway‑Modell betrachten wir als natürlichen Skalierungspfad, wenn Sie bereits ein „richtiges Business“ mit Dutzenden Märkten haben. Da das Gateway der natürliche nächste Schritt ist, schauen wir uns dennoch ein wichtiges Detail dieser Architektur an: wie man locale und das Land des Nutzers im Sitzungszustand speichert.
6. Locale als Teil des Clientzustands im Gateway
Bisher sind wir davon ausgegangen, dass jede Anfrage alles Nötige enthält. In der Praxis ist es jedoch bequem, einen Teil der Informationen im Sitzungszustand zu halten. Zum Beispiel:
- Der Nutzer kommt einmal mit locale = "ru-RU" und userLocation.country = "RU".
- Danach wollen Sie alle seine Anfragen auf das RU‑Backend routen, selbst wenn einige Zwischenaufrufe ohne explizite locale in den Argumenten kommen.
MCP hat ein nützliches Feld _meta["openai/subject"] — eine anonyme Nutzerkennung, die OpenAI an Ihre Services schickt. Diese kann man als Session‑Key verwenden.
Einfache In‑Memory‑Implementierung des Zustands
Wir schreiben eine winzige State‑Schicht im Gateway (natürlich sollte man im Produktivbetrieb statt Map besser Redis oder einen anderen externen Speicher verwenden).
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);
}
Jetzt kann man im Gateway‑Handler zuerst den Zustand aktualisieren und ihn dann bei der Backend‑Wahl verwenden:
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);
}
);
So „merken“ Sie sich einmal die Zuordnung clientId → locale, country und können sie in allen folgenden Tool‑Aufrufen nutzen, ohne Felder in jedem Argument zu duplizieren.
Genauso kann das Gateway die bevorzugte Währung, Preisformate oder andere Einstellungen speichern, die für Commerce‑Logik nützlich sind (mehr dazu im Modul zu ACP).
7. GiftGenius: zwei Szenarien und der Einfluss der Architekturwahl
Damit es nicht so wirkt, als würden wir nur abstrakte Kästchen diskutieren, schauen wir uns konkrete GiftGenius‑Szenarien an.
Szenario 1: Nutzer aus Russland, schreibt auf Russisch
Nehmen wir an:
- _meta["openai/locale"] = "ru-RU",
- _meta["openai/userLocation"].country = "RU".
Der Nutzer schreibt: „Such ein Geschenk für einen Kollegen, liebt Brettspiele, bis zu 3000 Rubel“.
In Modell 1 (ein MCP):
- Der Handler liest die locale aus _meta und erhält "ru-RU".
- Er lädt gift_catalog.ru.json, wo alle Bezeichnungen auf Russisch sind und die Preise in Rubel.
- Er filtert nach Kategorie und Budget und gibt eine strukturierte Geschenkliste auf Russisch zurück.
In Modell 2 (Gateway + Monos):
- Das Gateway liest locale und userLocation und entscheidet, dass es ein RU‑Nutzer ist.
- Es leitet den Aufruf suggest_gifts an mcp-giftgenius-ru weiter.
- Dieser arbeitet nur mit dem russischen Katalog und der Ozon‑API und liefert Geschenke in Rubel.
In beiden Fällen sieht der Nutzer alles in seiner Sprache, aber im zweiten Fall weiß Ihr englischer MCP‑Server nicht einmal, dass es einen Katalog für Russland gibt.
Szenario 2: Nutzer aus Deutschland, schreibt auf Englisch
Jetzt gilt:
- _meta["openai/locale"] = "en",
- _meta["openai/userLocation"].country = "DE".
Der Nutzer schreibt: „Gift for my German coworker, budget 50 EUR“.
In Modell 1:
- "en" liefert englische Texte,
- und "DE" bei country können Sie zur Wahl eines Katalogs nutzen, in dem die Preise in Euro sind und das Sortiment an Europa angepasst ist.
In Modell 2:
- Das Gateway kann entscheiden, dass locale = "en" → englischer Service, aber country = "DE" → Waren aus dem europäischen Lager; abhängig von Ihrer Business‑Logik können Sie:
- entweder die Anfrage an mcp-giftgenius-en mit dem Parameter country=DE schicken,
- oder einen separaten mcp-giftgenius-eu für Europa betreiben.
Hier sieht man gut, dass Locale (Sprache) und Region (userLocation) unterschiedliche Dimensionen sind und das Gateway ein geeigneter Ort ist, um sie zu einer Entscheidung zu verbinden: „welchen Service rufen wir und welche Produkte zeigen wir“.
8. Locale in Tool‑Schemas vs. locale nur in _meta
Unabhängig davon, ob Sie einen MCP oder die Kombination Gateway + einsprachige Services nutzen, lohnt sich zum Schluss noch ein feiner, aber wichtiger Punkt: locale nur in _meta halten oder als Tool‑Argument aufnehmen?
Es gibt zwei Ansätze.
Erstens: sich nur auf _meta verlassen.
Das ist bequem, weil die Tool‑Schemas nicht mit einem weiteren Feld überladen werden. Der Server liest locale aus extra._meta und entscheidet selbst. In Modell 1 reicht das oft aus.
Zweitens: locale (und eventuell currency) explizit im inputSchema des Tools vorsehen.
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"]
};
Im System‑Prompt können Sie das Modell anweisen, das Feld locale stets als Argument zu füllen und dabei den Nutzerkontext zu verwenden. Das macht die Intention transparent: In den JSON‑Argumenten ist direkt sichtbar, in welcher Sprache der Server arbeiten soll. Dieser Ansatz ist besonders hilfreich in komplexeren Architekturen, in denen es einen gemeinsamen MCP gibt, der intern anhand von locale auf verschiedene Services oder Ressourcen routet.
In der Praxis kombiniert man oft beide Ansätze: Im Schema gibt es ein Feld locale, aber falls das Modell es aus irgendeinem Grund nicht füllt, sichert sich der Server über _meta["openai/locale"] ab.
9. Wo verläuft die Grenze zwischen Lokalisierung und „überflüssiger Logik“ im Gateway
Eine Falle, in die man leicht gerät: Wenn wir schon ein smartes Gateway haben, dann soll es doch:
- selbst entscheiden, welche Geschenke gezeigt werden,
- selbst Daten und Preise formatieren,
- selbst Klick‑Reports aggregieren usw.
Das klingt verlockend, macht aus dem Gateway jedoch einen „zweiten Monolithen“ und erschwert Updates und Betrieb. In industriellen API‑Gateway‑Praktiken (und ein MCP‑Gateway hat dieselbe Rolle) liegt der Fokus auf einigen wenigen Aufgaben: Authentifizierung, Autorisierung, Routing und leichtes Kontext‑Enrichment. Beispielsweise kann das Gateway HTTP‑Header in praktische Metadaten transformieren. Business‑Logik und schwere Operationen sollten in den Backend‑Services leben.
Für die Lokalisierung heißt das:
- Das Gateway darf _meta["openai/locale"] und _meta["openai/userLocation"] parsen.
- Es darf sie im Clientzustand speichern.
- Es darf den passenden Sprachserver wählen oder dem Request ein Feld locale/country hinzufügen.
Aber die Geschenkeauswahl, Filterung nach Alter, Budget etc. — all das sollte in den MCP‑Backends bleiben.
10. Typische Fehler beim Entwurf der Lokalisierung mit MCP und Gateway
Fehler Nr. 1: Sich ausschließlich auf „Spracherkennung“ aus dem Nutzertext zu verlassen.
Man möchte manchmal den Nachrichtentext nehmen, durch einen Language‑Detector jagen und auf dieser Grundlage entscheiden, welchen Server man ruft. Das kann ein nützlicher Fallback sein, aber kein primärer Mechanismus. Die Plattform liefert bereits openai/locale und openai/userLocation, die die ChatGPT‑Einstellungen und das Nutzerumfeld berücksichtigen. Diese Signale zu ignorieren und „Sprache raten“ zu spielen, ist ein effizienter Weg, UX in den unerwartetsten Fällen zu brechen.
Fehler Nr. 2: locale nur im Kopf des Modells halten und nicht zum Server übermitteln.
Wenn locale weder in _meta noch in den Tool‑Argumenten vorkommt, weiß der Server nichts über die Sprache des Nutzers. Das Modell kann natürlich versuchen, die Zeichenkette „книги“ in books zu übersetzen, aber das ist unzuverlässig, besonders bei komplexen Kategorien. Der richtige Weg ist, locale explizit zu übergeben: entweder als Argument locale oder indem man es aus _meta liest und die Architektur darum baut.
Fehler Nr. 3: Die gesamte Business‑Logik der Lokalisierung ins Gateway verlagern.
Wenn das Gateway beginnt, selbst Geschenke auszuwählen, Datenbanken zu kontaktieren und mit externen APIs zu kämpfen, hört es auf, ein leichtgewichtiger Router zu sein, und wird zu einem schweren Service, der schwer zu skalieren und zu aktualisieren ist. Am Ende erhält man zwei Monolithen statt eines. Halten Sie das Gateway lieber maximal „dumm“: Es schaut auf locale/userLocation, wählt das richtige Backend und reicht die Metadaten sauber weiter.
Fehler Nr. 4: Routing hart nur an IP oder userLocation binden.
Man möchte es manchmal einfach machen: „Wenn das Land RU ist — gehen wir auf den RU‑Server“. Aber ein Nutzer kann sich in Deutschland befinden und trotzdem eine russische Oberfläche wollen oder mitten in der Session „switch to English“ verlangen. Wenn Sie im Gateway openai/locale und den möglichen Wunsch des Nutzers, die Sprache zu wechseln, nicht berücksichtigen, wird das Routing „betoniert“ und bricht die UX. Stützen Sie sich besser auf die Kombination aus locale und userLocation und halten Sie die Möglichkeit bereit, Einstellungen über den Sitzungszustand zu überschreiben.
Fehler Nr. 5: _meta["openai/subject"] nicht nutzen und alle Parameter in jedem Argument duplizieren.
Wenn Sie in jedem Tool‑Argument beginnen, locale, country, currency, userId und den halben Rest durchzuschleusen, wird das Leben schnell trist. MCP übermittelt bereits eine anonyme Nutzerkennung via _meta["openai/subject"], und Sie können all diese Informationen im Clientzustand auf der Seite des Gateways oder des Backend‑Servers speichern. Das vereinfacht die Verträge und reduziert das Risiko der Argument‑Desynchronisierung.
Fehler Nr. 6: Keine Evolutionsstrategie: „gleich ein komplexes Gateway für zehn Sprachen bauen“.
Oft möchte man es sofort perfekt machen: Gateway, fünf Sprachen, drei Regionen, zehn MCP‑Services. In der Praxis ist es einfacher, mit dem Modell „ein MCP + locale‑Parameter oder _meta“ zu starten, das Verhalten zu stabilisieren und dann Gateway und einsprachige Services mit dem Wachstum herauszulösen. Der Versuch, sofort einen riesigen „Zoo“ zu bauen, verzögert den Release fast garantiert und verkompliziert das Debugging.
GO TO FULL VERSION