1. Зачем вообще нужен ещё один слой?
Почти все начинают одинаково: есть один MCP‑сервер, он описывает пару инструментов, ChatGPT ходит к нему напрямую по HTTPS, и всё, казалось бы, прекрасно. Условная архитектура выглядит так:
ChatGPT → ваш MCP-сервер → база / внешние API
На стадии «pet project» это действительно нормальный вариант. Но как только приложение начинает обрастать функциональностью, а команда разработки — увеличиваться, очень быстро всплывают проблемы.
Во‑первых, MCP‑сервер превращается в «God-объект». В нём одновременно живут и инструменты подбора подарков, и checkout, и аналитика, и что‑нибудь ещё из серии «а давайте сюда же засунем отчётность». Разные части кода имеют разные SLA и разные требования к безопасности, но склеены в один процесс.
Во‑вторых, ChatGPT и другие клиенты вынуждены знать топологию ваших сервисов. Если через полгода у вас появится ещё один MCP‑сервер для commerce, вам придётся переподключать клиентов, менять конфиги и описания. Вместо «одной точки входа» у вас получается зоопарк URL‑ов.
В‑третьих, становится непонятно, где реализовывать общее для всех сервисов: аутентификацию, логирование, метрики, rate limiting, проверку токенов, локализацию и маршрутизацию по регионам. Если раскидать это по всем MCP/Agent‑сервисам, вы получите много дублирования и разное поведение в разных сервисах.
Чтобы разорвать эту связность и одновременно спрятать внутреннюю сложность от ChatGPT, в игру вступает MCP Gateway — сетевой шлюз и единая точка входа для всего MCP‑трафика.
2. Что такое MCP Gateway в контексте ChatGPT App
Формально MCP Gateway — это прокси-слой и единая точка входа между MCP-клиентами (ChatGPT, MCP Jam, ваши внутренние инструменты) и набором ваших backend-сервисов — обычно это REST/HTTP-API, микросервисы, Agents-службы, commerce-backend и т.п.
Gateway сам реализует MCP-протокол наружу (для ChatGPT он выглядит как один MCP-сервер), а внутри просто ходит в обычные REST-endpoint’ы по HTTP/gRPC.
На запрос tools/list gateway не проксирует вызов дальше, а отдаёт собственный список инструментов: он либо жёстко описан в коде, либо собирается из конфигурации. Каждый tool привязан к конкретному REST-endpoint’у и схеме данных. На запрос tools/call gateway берёт имя инструмента, находит соответствующий REST-маршрут и вызывает его через fetch/HTTP-клиент.
Схематично это можно представить так:
flowchart LR
ChatGPT["ChatGPT / модель"] --> |MCP JSON-RPC| Gateway["MCP Gateway<br/>(единственный MCP-сервер)"]
Gateway --> GiftAPI["Gift REST API<br/>/ микросервис подарков"]
Gateway --> CommerceAPI["Commerce REST API<br/>/ ACP / платежи"]
Gateway --> AnalyticsAPI["Analytics Service<br/>/ события и метрики"]
Для ChatGPT это один сервер: один URL, один набор инструментов, один поток событий. Для вас — гибкая точка маршрутизации трафика к разным холодным и горячим сервисам.
3. MCP Gateway в архитектуре GiftGenius
Чтобы меньше говорить абстракциями и показать gateway «в живой системе», продолжим наш пример GiftGenius — приложения, которое подбирает подарки и умеет оформлять заказы через ACP/Instant Checkout.
В простой версии у нас был один MCP‑сервер, который умел и suggest_gifts, и checkout_start. Теперь, когда приложение разрослось, мы разделяем обязанности:
- Gift REST API — поиск и рекомендации подарков, работа с каталогом и фидом (обычный HTTP/REST-сервис).
- Commerce REST API — ACP, сессии checkout, статусы заказов, связь с платёжным провайдером.
- Analytics Service / REST API — сбор событий и метрик (какие подборки открывают, что покупают).
- Отдельный Agents-сервис (если нужен) — сложные многошаговые сценарии. Он тоже доступен через HTTP/REST, а не через MCP.
MCP Gateway становится единой точкой входа для всех этих компонентов. Он:
- на запрос tools/list отдаёт единый список tools, который сам описывает: каждый tool привязан к конкретному REST-endpoint’у одного из сервисов;
- на запрос tools/call смотрит на имя инструмента (params.name), по таблице маршрутизации определяет, в какой REST-сервис идти, и вызывает соответствующий HTTP-метод (через fetch, axios и т.п.).
Если приходит tools/call с именем suggest_gifts, gateway вызывает соответствующий REST-endpoint в Gift REST API. Если это checkout_start, запрос уйдёт в Commerce REST API.
Небольшой псевдокод на TypeScript в стиле Express может выглядеть так:
// Очень упрощённый обработчик MCP-запросов
app.post("/mcp", async (req, res) => {
const mcpReq = req.body as { method: string; params?: any };
const ctx = buildContextFromHeaders(req); // auth, locale и т.п.
const toolName = mcpReq.params?.name;
const backendRes = await callBackend(toolName, mcpReq, ctx);
res.json(backendRes);
});
Внутри pickBackend вы можете опираться на имя метода, имя инструмента, локаль пользователя и даже на версию сервиса (для «канареечных» и blue/green релизов, о которых будем говорить позже в модуле).
4. Обязанности MCP Gateway: что он точно делает
Мы посмотрели, как gateway вписывается в архитектуру GiftGenius. Теперь давайте явно зафиксируем, какие обязанности у него есть как у отдельного слоя, независимо от конкретного приложения. Важно воспринимать gateway как сетевой и кросс‑сервисный слой. Его задача — не думать о бизнес‑логике подарков, а решать инфраструктурные задачи вокруг них.
Маршрутизация запросов
Первая роль — маршрутизатор. Gateway получает MCP‑запрос и, опираясь на его содержимое, контекст пользователя и свою конфигурацию, выбирает целевой сервис.
Например, в GiftGenius можно завести простую таблицу маршрутизации:
const TOOL_ROUTES: Record<string, "gift" | "commerce" | "analytics"> = {
suggest_gifts: "gift",
get_similar_gifts: "gift",
checkout_start: "commerce",
get_order_status: "commerce",
log_event: "analytics",
};
И затем использовать её:
function pickBackend(req: McpRequest, ctx: GatewayContext): Backend {
if (req.method === "tools/list") return "aggregator";
if (req.method === "tools/call") {
const toolName = req.params?.name;
const group = TOOL_ROUTES[toolName] ?? "gift";
return group === "commerce" ? commerceBackend : giftBackend;
}
return giftBackend;
}
В нашем случае giftBackend, commerceBackend, analyticsBackend — это обычные REST-сервисы: у каждого есть базовый URL ("https://gift-api.internal", "https://commerce-api.internal", …). Gateway не пересылает MCP внутрь, он раскладывает MCP-вызов на HTTP-запрос к нужному REST-endpoint’у.
Аутентификация и авторизация на периметре
Вторая ключевая функция — защита периметра. Gateway — удобное место, чтобы проверить токен, понять, кто пользователь, от какой организации он пришёл, и какие permissions у него есть.
Он может, например, принять OAuth‑токен от ChatGPT или от вашего MCP Auth‑сервера, провалидировать его (желательно с помощью проверенной библиотеки, а не самописной криптографии) и превратить в аккуратный объект контекста:
type GatewayContext = {
userId: string | null;
tenantId: string | null;
locale: string;
};
function buildContextFromHeaders(req: Request): GatewayContext {
const token = req.headers["authorization"]; // "Bearer ..."
const claims = token ? verifyJwt(token) : null;
return {
userId: claims?.sub ?? null,
tenantId: claims?.tenant ?? null,
locale: (req.headers["x-openai-locale"] as string) || "en-US",
};
}
Внутренние backend-/REST-сервисы тогда могут не париться с разбором сырых HTTP‑заголовков и токенов, а получать уже нормализованный context с userId, tenantId и locale. Рекомендации MCP‑доков прямо говорят: не реализуйте валидацию токенов «с нуля», а используйте проверенные библиотеки и короткоживущие токены.
Логирование, трейсинг и метрики
Третья роль — наблюдаемость. Gateway видит все входящие MCP‑запросы и все ответы, поэтому это идеальное место, чтобы проставить correlation‑id, залогировать параметры инструментов (без чувствительных данных), записать время ответа и статус.
Простейшая идея:
app.use((req, res, next) => {
const requestId = crypto.randomUUID();
(req as any).requestId = requestId;
const start = Date.now();
res.on("finish", () => {
const ms = Date.now() - start;
console.log(
`[${requestId}] ${req.method} ${req.url} -> ${res.statusCode} in ${ms}ms`
);
});
next();
});
Позже в модуле наблюдаемости вы сможете отправлять эти данные не просто в console.log, а в структурное хранилище и строить по ним дашборды.
Базовый контроль нагрузки
Четвёртая, но тоже важная задача — первичный контроль нагрузки. На gateway удобно поставить счётчики вызовов по пользователям, организациям, инструментам и endpoint‑ам, чтобы не дать одному «бешеному» клиенту сжечь ваш кластер и бюджет на модели.
В этом модуле мы пока только фиксируем идею: rate limiting и очереди живут на уровне gateway, детали реализации (Redis, токен‑бакеты, ликинг‑бакеты) будут разбираться в следующей лекции про защиту периметра.
Обогащение запросов контекстом
И, наконец, gateway — хорошее место, чтобы превратить сырой контекст MCP‑клиента в аккуратные аргументы для внутренних инструментов.
Например, ChatGPT может передавать локаль пользователя через openai/locale и _meta["openai/userLocation"]. Gateway может:
- выбрать нужный региональный сервис (ru‑сервер, en‑сервер и т.д.);
- добавить locale в аргументы tool‑вызова, даже если сам инструмент его не запрашивал явно в JSON Schema (например, как необязательное поле).
Условно:
function enrichToolArgs(args: any, ctx: GatewayContext) {
return {
...args,
locale: args.locale ?? ctx.locale,
tenantId: ctx.tenantId,
};
}
В результате Gift API получает сразу «богатый контекст» и может, например, подтянуть русские описания подарков для "ru-RU" и английские для "en-US".
5. Что MCP Gateway делать НЕ должен
Когда у разработчика появляется «магическое место, через которое проходит всё», у него возникает естественное желание запихнуть туда всё, что раньше лежало в отдельных сервисах. Так gateway рискует превратиться в монстра.
Есть несколько вещей, которые, как правило, не должны жить в этом слое.
Во‑первых, сложная бизнес‑логика. Подбор подарков, правила скидок, расчёт стоимости доставки, логика ACP — всё это должно оставаться внутри специализированных backend-/commerce-сервисов. Gateway может максимум сделать лёгкую предварительную валидацию (например, проверить, что цена не отрицательная), но не выбирать SKU и не считать налог по регионам.
Во‑вторых, долгоживущее пользовательское состояние. Gateway — это типичный stateless‑сервис. Он должен спокойно масштабироваться горизонтально, не полагаться на локальную память и перезапускаться без последствий. Если вы начнёте хранить в нём, например, состояние checkout‑мастера или временное содержимое корзины, вы быстро получите боли с синхронизацией между инстансами.
В‑третьих, специфические функции, которые логичнее расположить внутри самих backend-сервисов (Gift API, Commerce API). Например, если Gift backend хочет кэшировать результат поиска подарков, пусть делает это сам, возможно используя Redis. Gateway не обязан знать про эту внутреннюю оптимизацию. Мы отдельно ещё будем говорить про защиту периметра, и там как раз подчёркивается: шлюз — про сетевые и кросс‑сервисные функции, а не про бизнес‑правила рекомендаций.
В‑четвёртых, тяжёлые вычисления. Если внутри gateway вы начнёте дергать LLM‑модели, делать сложные трансформации и агрегации, он перестанет быть «лёгким» фронтом и превратится в ещё один толстый backend, который сложно масштабировать и отлаживать.
6. Gateway, локализация и версии сервисов
Мы разобрали базовые обязанности gateway и то, чего в него лучше не класть. Теперь посмотрим на пару типичных «продвинутых» задач, которые удобно решать именно на этом слое: локализацию и версионирование сервисов. Ещё одна интересная роль gateway — умный роутинг по локали и версиям сервисов.
Когда ChatGPT вызывает ваш App, у него уже есть представление о языке пользователя (openai/locale) и, часто, о его геолокации (_meta["openai/userLocation"]). Gateway может использовать эту информацию, чтобы отправлять запросы на нужные backend‑сервисы.
Например, можно построить архитектуру «один gateway — много моноязыковых backend‑серверов»:
- ru‑Gift API — только русский каталог подарков и тексты.
- en‑Gift API — только английский.
- jp‑Gift API — японский (когда вы решите захватить мир).
Gateway в этом случае выступает MCP‑сервером для ChatGPT и по locale и userLocation выбирает нужный внутренний сервис.
Условно:
function pickGiftBackendByLocale(ctx: GatewayContext): Backend {
if (ctx.locale.startsWith("ru")) return giftRuBackend;
if (ctx.locale.startsWith("ja")) return giftJpBackend;
return giftEnBackend;
}
Там же удобно реализовывать простейший канареечный роутинг. В этом модуле по production‑архитектуре мы как раз предлагаем использовать gateway, чтобы часть трафика отправлять в новый кластер сервиса, а остальное — в старый.
Пример очень грубой канарейки:
function pickGiftBackendCanary(ctx: GatewayContext): Backend {
const hash = hashUser(ctx.userId ?? "anonymous");
const bucket = hash % 100;
return bucket < 5 ? giftBackendV2 : giftBackendV1; // 5% трафика идёт на v2
}
Так можно безопасно выкатывать новую версию Gift API, глядя на метрики и ошибки, не ломая весь прод сразу.
7. Типовые архитектуры: от «всё в одном» к Gateway
Ранее в курсе вы уже видели несколько вариантов production‑архитектуры ChatGPT App. В этом модуле мы выделяем три базовые топологии, которых в 90% случаев достаточно.
Первая — «всё в одном». App‑виджет (Next.js), MCP‑сервер, Agents‑логика и простой commerce‑backend живут в одном сервисе, часто в одном репозитории и даже в одной Vercel‑апке. Плюс такого подхода — почти нет DevOps, деплой простой, latency минимальный. Минус — масштабировать отдельные части сложно, одна горячая фича может положить всё приложение, а границы между компонентами размыты.
Вторая — App + MCP Gateway + несколько backend‑сервисов. Здесь Next.js‑виджет живёт отдельно (например, на Vercel), а весь MCP‑трафик идёт через Gateway, который маршрутизирует запросы к Gift REST API, Commerce REST API, Agents-службе, ACP-backend и другим. Это как раз та схема, которую мы сейчас разбираем в рамках GiftGenius и которая подходит для 90% реальных production‑кейсов.
Третья — то же самое, но в нескольких регионах (multi‑region), с глобальным балансировщиком перед gateway. Тогда пользователь из Европы попадает в eu‑кластер, из США — в us‑кластер, а каждый регион строится по схеме «Gateway + несколько backend‑сервисов». Это уже история для достаточно крупных проектов с глобальной аудиторией.
Для нас важно сейчас не столько запомнить все варианты, сколько привыкнуть мыслить gateway как отдельным логическим компонентом архитектуры, даже если на первых этапах его роль исполняет, скажем, один MCP‑монолит или backend вашего App.
8. Где физически живёт MCP Gateway
Хорошая новость: MCP Gateway — это не обязательно огромный отдельный сервис на Kubernetes. Чаще всего он проходит несколько стадий взросления.
На самом маленьком масштабе роль gateway может выполнять сам MCP‑сервер. В этом случае вам просто нужно аккуратно структурировать код: вынести маршрутизацию, аутентификацию и логирование в один модуль, а логику инструментов — в другие. В этом модуле мы прямо отмечаем, что в маленьких системах функции gateway могут быть внутри MCP‑сервера или backend‑части App (например, в Next.js API route).
Следующий шаг — отдельный Node/TypeScript‑сервис. Это может быть Express/Fastify‑приложение, которое слушает "/mcp" и ходит внутрь в несколько HTTP‑сервисов. Для многих команд это комфортный вариант: он хорошо ложится на привычные DevOps‑инструменты.
Простейший скелет такого сервиса:
const app = express();
app.use(express.json());
app.post("/mcp", handleMcpRequest); // здесь вся магия gateway
app.listen(4000, () => {
console.log("MCP Gateway listening on :4000");
});
На ещё более взрослом этапе gateway можно реализовать поверх managed‑решений: AWS API Gateway, Cloudflare Workers/Routes, NGINX/Envoy с конфигом роутинга и Lua/JS‑скриптами. Важно понимать, что это изменение реализации, а не концепции. Архитектурно ChatGPT по‑прежнему ходит в одну точку, а все детали разбирает gateway.
9. Мини‑пример: простой MCP Gateway для GiftGenius
Мы уже по отдельности посмотрели на маршрутизацию, контекст и обработку tools/list. Теперь соберём всё сказанное в один чёткий, но маленький пример. Пусть у нас есть два внутренних REST‑сервиса:
- GIFT_API_BASE = "https://gift-api.internal";
- COMMERCE_API_BASE = "https://commerce-api.internal".
И один gateway, к которому ChatGPT будет ходить по адресу "https://gateway.giftgenius.com/mcp".
Сначала определим пару типов:
type Backend = "gift" | "commerce";
type ToolRoute = {
backend: Backend;
method: "GET" | "POST";
path: string;
};
const TOOL_ROUTES: Record<string, ToolRoute> = {
suggest_gifts: {
backend: "gift",
method: "POST",
path: "/api/gifts/suggest",
},
checkout_start: {
backend: "commerce",
method: "POST",
path: "/api/checkout/start",
},
get_order_status: {
backend: "commerce",
method: "GET",
path: "/api/orders/status",
},
};
Дальше реализуем выбор backend’а и вызов:
async function callBackend(toolName: string, mcpReq: McpRequest, ctx: GatewayContext) {
const route = TOOL_ROUTES[toolName];
if (!route) {
throw new Error(`Unknown tool: ${toolName}`);
}
const base =
route.backend === "gift" ? GIFT_API_BASE : COMMERCE_API_BASE;
const url = base + route.path;
// args, которые пришли в MCP-вызове tools/call
const args = {
...(mcpReq.params?.arguments ?? {}),
locale: ctx.locale,
};
const res = await fetch(url, {
method: route.method,
headers: { "content-type": "application/json" },
body: route.method === "POST" ? JSON.stringify(args) : undefined,
});
const data = await res.json();
// Заворачиваем ответ REST-сервиса в MCP-ответ
return {
result: data,
} satisfies McpResponse;
}
И наконец, основной обработчик, который:
- Строит контекст из заголовков (auth, locale).
- Выбирает backend.
- Либо агрегирует tools/list, либо проксирует tools/call.
app.post("/mcp", async (req, res) => {
const mcpReq = req.body as McpRequest;
const ctx = buildContextFromHeaders(req);
if (mcpReq.method === "tools/list") {
// Gateway сам объявляет инструменты и их схемы
const tools = [
{
name: "suggest_gifts",
description: "Подбирает подарки по бюджету и интересам.",
inputSchema: { /* ... JSON Schema ... */ },
},
{
name: "checkout_start",
description: "Создаёт черновик заказа и запускает checkout.",
inputSchema: { /* ... */ },
},
// ...
];
return res.json({ result: { tools } });
}
if (mcpReq.method === "tools/call") {
const toolName = mcpReq.params?.name;
const backendRes = await callBackend(toolName, mcpReq, ctx);
return res.json(backendRes);
}
res.status(400).json({ error: { message: "Unsupported MCP method" } });
});
Это, конечно, упрощённая схема, но она уже показывает ключевые идеи:
- gateway не знает, как именно Gift API подбирает подарки;
- он лишь аккуратно маршрутизирует, обогащает аргументы и, при желании, логирует и лимитирует вызовы.
10. Как всё это связано с дальнейшими темами модуля
MCP Gateway — фундаментальная часть всего, что мы будем обсуждать в оставшихся лекциях модуля:
- В следующей теме мы будем говорить о защите периметра: rate limiting, очередях и backpressure. Всё это живёт прежде всего на уровне gateway, потому что именно он видит весь входящий трафик и может «отрезать лишнее» до того, как запросы завалят backend.
- Дальше мы обсудим устойчивость: timeouts, circuit breakers, bulkheads. Gateway — точка, где удобно централизованно задавать таймауты на внешние вызовы и включать/выключать проблемные сервисы (например, временно «отрубить» Commerce API, если он уходит в ошибки).
- И наконец, при разговоре о масштабировании и деплое мы будем смотреть на gateway как на отдельный кластер, который можно балансировать, катить по схемам blue/green и canary и откатывать независимо от внутренних MCP‑сервисов.
По сути, если раньше вы думали «у меня есть App и MCP‑сервер», то теперь схема расширяется до «у меня есть App, MCP Gateway, несколько backend-/Agents-кластеров и commerce-backend». И именно gateway позволяет при этом не усложнять конфигурацию для ChatGPT — он по‑прежнему видит одну MCP‑точку.
11. Типичные ошибки при работе с MCP Gateway
Ошибка №1: превращать gateway в «бизнес‑монстра».
Частая ловушка: раз через gateway проходит всё, то почему бы не добавить туда расчёт скидок, подбор SKU, сложные правила категорий или валидацию промокодов. В итоге вы получаете жирный сервис, который сложно масштабировать и менять, и теряете смысл разделения на Gift API, Commerce API и прочие специализированные компоненты. Лучше держать gateway как тонкий сетевой слой, а всё доменно‑специфическое оставить внутри профильных сервисов.
Ошибка №2: хранить в gateway долгоживущее пользовательское состояние.
Идея «а давайте запомним корзину пользователя прямо в памяти gateway» звучит заманчиво, пока у вас один инстанс. Как только появляется второй, начинается боль: где настоящая корзина — в инстансе A или B? Что будет после рестарта? Gateway должен оставаться stateless: максимум — небольшой кэш handshake’ов или конфигов, а всё состояние сессий и заказов хранится в БД или в специализированных сервисах.
Ошибка №3: делать ChatGPT осведомлённым о внутренней топологии сервисов.
Если вы начинаете прокидывать в ChatGPT сразу несколько API‑серверов (отдельно Gift API, отдельно Commerce API), а gateway использовать «местами», вы теряете главное преимущество: единый вход и централизованный контроль. При смене топологии придётся менять конфигурацию в нескольких местах. Гораздо проще один раз настроить MCP Gateway как официальный endpoint для App и скрыть все внутренние перемены за ним.
Ошибка №4: дублировать кросс‑сервисную логику во всех backend’ах.
Иногда команды пытаются реализовать аутентификацию, rate limiting, логирование и локализацию в каждом REST‑сервисе по отдельности. В итоге политика прав и лимитов в Gift API одна, в Commerce API — другая, и поведение App становится непредсказуемым. Gateway как раз и нужен, чтобы эти вещи централизовать: проверить токен, определить tenant и locale, залогировать вызов, применить лимиты, а дальше уже идти в конкретный сервис.
Ошибка №5: перегружать gateway тяжёлыми вычислениями и LLM‑вызовами.
Технически никто не мешает вам из gateway вызывать ещё одну LLM‑модель, делать сложные агрегации или долгие batch‑операции. Но это быстро превратит его в ещё один тяжёлый backend, который невозможно нормально масштабировать и изолировать. Gateway должен оставаться быстрым и предсказуемым: максимум лёгкая трансформация и маршрутизация. Всё тяжёлое — внутрь REST‑сервисов или в очереди/воркеры, о которых мы поговорим дальше в модуле.
Ошибка №6: слишком рано усложнять инфраструктуру.
Обратная крайность — сразу городить отдельный Kubernetes‑кластер, NGINX‑стек, Cloudflare Workers и кучу сложных конфигов ради маленького учебного App. В этом нет смысла, пока у вас нет реальной нагрузки и требований к отказоустойчивости. Вполне нормально начинать с одного MCP‑монолита или простого Node‑gateway и только потом, по мере роста, выносить отдельные компоненты в кластера и managed‑сервисы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ