CodeGym /课程 /ChatGPT Apps /MCP Gateway:为什么需要把它放在 ChatGPT 与你的服务之间

MCP Gateway:为什么需要把它放在 ChatGPT 与你的服务之间

ChatGPT Apps
第 16 级 , 课程 0
可用

1. 为什么还需要再加一层?

几乎所有人一开始都差不多:有一个 MCP 服务器,它暴露了几个工具,ChatGPT 通过 HTTPS 直接访问它,看起来一切都很好。示意架构如下:

ChatGPT  →  你的 MCP 服务器  →  数据库 / 外部 API

在 “pet project” 阶段,这确实是个正常方案。但一旦应用功能不断扩展、研发团队开始扩大,问题就会很快浮现。

首先,MCP 服务器会变成“God 对象(God‑object)”。在同一个进程里同时塞进了礼物挑选工具、checkout、分析统计,甚至“要不把报表也顺便塞进来”。不同代码部分的 SLA 和安全要求各不相同,却被糅合在一个进程里。

其次,ChatGPT 和其他客户端被迫了解你的服务拓扑。如果半年后你又新建了一个给 commerce 用的 MCP 服务器,你就得重新为客户端接线、改配置和描述。你原本想要的“单一入口”反而变成了一堆零散的 URL。

第三,很多跨服务的共性能力该放哪儿就变得不清晰:认证、日志、指标、限流、令牌校验、本地化、按地域路由等。如果把这些分散到每个 MCP/Agent 服务里,你会产生大量重复代码,并在不同服务里得到不一致的行为。

为了解耦并把内部复杂性对 ChatGPT 隐藏起来,我们引入 MCP Gateway——它是网络网关,也是所有 MCP 流量的统一入口。

2. 在 ChatGPT App 语境下,什么是 MCP Gateway

形式上,MCP Gateway 是 MCP 客户端(ChatGPT、MCP Jam、你的内部工具)与一组后端服务之间的代理层与统一入口——通常这些后端是 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. GiftGenius 架构中的 MCP Gateway

为了减少空谈并展示 gateway 在“真实系统”里的样子,我们继续使用 GiftGenius 例子——这是一个能挑选礼物并通过 ACP/Instant Checkout 下单的应用。

在简单版本里,我们只有一个 MCP 服务器,同时支持 suggest_giftscheckout_start。现在应用变大了,我们把职责拆分出来:

  • Gift REST API——礼物搜索与推荐、目录与 feed(常规 HTTP/REST 服务)。
  • Commerce REST API——ACP、checkout 会话、订单状态、与支付提供方对接。
  • Analytics Service / REST API——事件与指标收集(打开了哪些推荐、买了什么)。
  • 独立的 Agents 服务(如需要)——复杂的多步场景。它同样通过 HTTP/REST 暴露,而不是通过 MCP。

MCP Gateway 成为这些组件的统一入口。它会:

  • tools/list 请求上返回统一的工具清单,清单由它自身定义:每个 tool 都映射到某个服务的具体 REST endpoint;
  • tools/call 请求上读取工具名(params.name),依据路由表确定目标 REST 服务,并调用相应的 HTTP 方法(通过 fetchaxios 等)。

如果来的 tools/call 的工具名是 suggest_gifts,gateway 就会调用 Gift REST API 中对应的 REST endpoint;如果是 checkout_start,请求则会发往 Commerce REST API。

一个 Express 风格的 TypeScript 伪代码大致如下:

// 极度简化的 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;
}

在我们的场景里,giftBackendcommerceBackendanalyticsBackend 都是常规的 REST 服务:各自有一个基础 URL("https://gift-api.internal""https://commerce-api.internal" 等)。Gateway 并不会把 MCP 原样转发到内部,而是把 MCP 调用拆解成发往对应 REST endpoint 的 HTTP 请求。

边界处的认证与授权

第二个关键职责是保护边界。Gateway 是检验令牌、识别用户与其所属组织、判定其权限的最佳位置。

例如,它可以接收来自 ChatGPT 或你自家 MCP Auth 服务器的 OAuth 令牌,用成熟库(而不是自写加密)来校验,并转化为一个规范的上下文对象:

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,包含 userIdtenantIdlocale。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 维度计数,避免某个“失控”的客户端烧光你的集群与模型预算。

本模块先强调理念:限流与队列应落在 gateway 层。实现细节(Redis、令牌桶、漏桶等)将在下一节“边界保护”里展开。

以上下文丰富请求

最后,gateway 也是把 MCP 客户端的原始上下文转化为内部工具可直接使用的参数的好地方。

例如,ChatGPT 会通过 openai/locale_meta["openai/userLocation"] 传递用户的本地化信息。Gateway 可以:

  • 选择对应地域的服务(ru 服务器、en 服务器等);
  • locale 注入到工具调用参数中,即便该工具在 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 应是典型的无状态服务。它应能水平扩展、不依赖本地内存、并可无痛重启。如果你在其中保存 checkout 向导的状态或购物车临时内容,你会很快在多个实例之间的状态同步上吃尽苦头。

第三是更适合放在后端服务内部的特定功能(Gift API、Commerce API)。例如,若 Gift backend 想缓存礼物搜索结果,就让它在内部做(也许使用 Redis)。Gateway 不必知晓这些内部优化。我们在“边界保护”里也会再次强调:网关负责网络与跨服务能力,而非推荐的业务规则。

第四是重型计算。如果在 gateway 内调用 LLM 模型、做复杂转换与聚合,它就不再是“轻量前端”,而会变成另一个厚重的后端,难以扩展与调试。

6. Gateway、本地化与服务版本

我们已经讨论了 gateway 的基础职责和不该做的事。现在来看两个在该层实现起来很方便的“进阶”任务:本地化与服务版本化。Gateway 的另一项有趣职责,就是根据本地化与服务版本做智能路由。

当 ChatGPT 调用你的 App 时,它已经掌握了用户语言(openai/locale)以及常见的地理位置(_meta["openai/userLocation"])。Gateway 可以据此把请求路由到合适的后端服务。

例如,可以采用“一个 gateway—多个单语后端”的架构:

  • ru‑Gift API——只有俄语目录与文本。
  • en‑Gift API——只有英语。
  • jp‑Gift API——日语(当你准备征服世界时)。

在这种情况下,Gateway 作为 ChatGPT 的 MCP 服务器,会依据 localeuserLocation 选择合适的内部服务。

示例:

function pickGiftBackendByLocale(ctx: GatewayContext): Backend {
  if (ctx.locale.startsWith("ru")) return giftRuBackend;
  if (ctx.locale.startsWith("ja")) return giftJpBackend;
  return giftEnBackend;
}

同时也适合做最简单的金丝雀路由。在本生产架构模块里,我们建议使用 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

在课程前面你已经看过几种 ChatGPT App 的生产架构。在本模块我们归纳三种基础拓扑,足以覆盖 90% 的场景。

第一种——“一体化”。App 小部件(Next.js)、MCP 服务器、Agents 逻辑与简单的 commerce backend 都在一个服务里,常见是一个仓库,甚至一个 Vercel 应用。优点:几乎没有 DevOps、部署简单、延迟最小。缺点:难以单独扩展某一部分,某个热点功能可能拖垮整个应用,组件边界也容易模糊。

第二种——App + MCP Gateway + 多个后端服务。Next.js 小部件单独部署(例如在 Vercel),所有 MCP 流量经由 Gateway,再分别路由到 Gift REST API、Commerce REST API、Agents 服务、ACP backend 等。这正是我们在 GiftGenius 中讲解的方案,适用于 90% 的真实生产案例。

第三种——相同架构,但跨多个地域(multi‑region),在 gateway 前有全局负载均衡。来自欧洲的用户进入 eu 集群,来自美国的进入 us 集群,每个地域内部都遵循“Gateway + 多个后端服务”的模式。此方案适用于面向全球用户的较大项目。

此刻对我们更重要的不是记住所有变体,而是养成把 gateway 当作独立逻辑组件来思考的习惯,即便在早期阶段,它的职责由单一 MCP 单体或 App 的后端来承担。

8. MCP Gateway 应该部署在哪里

好消息是:MCP Gateway 不一定是一个在 Kubernetes 上运行的庞然大物。更多时候,它会经历几个“成长阶段”。

在最小规模时,MCP 服务器本身就可以扮演 gateway 的角色。这要求你在代码结构上更讲究:把路由、认证与日志抽成一个模块,而把工具逻辑放在其他模块。本模块也明确指出,在小型系统中,gateway 的功能可以落在 MCP 服务器或 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:AWS API Gateway、Cloudflare Workers/Routes、带有路由配置与 Lua/JS 脚本的 NGINX/Envoy。务必理解,这只是实现方式的变化,而非概念变化。从架构上看,ChatGPT 仍旧访问单一入口,具体细节由 gateway 处理。

9. 迷你示例:GiftGenius 的一个简单 MCP Gateway

我们已分别看过路由、上下文与 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",
  },
};

然后实现后端选择与调用:

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;

  // 在 MCP 的 tools/call 调用里传来的 args
  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;
}

最后是主要处理器,它会:

  1. 从请求头构建上下文(auth、locale)。
  2. 选择后端。
  3. 要么聚合 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: "创建订单草稿并启动结账流程。",
        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 是我们在本模块剩余课程里讨论的一切内容的基础:

  • 下一节我们会谈边界保护:限流、队列与背压。这些首先应落在 gateway 层,因为它能看到全部入站流量,能在请求压垮后端之前“截断多余”。
  • 接着讨论韧性:超时、熔断器、隔离舱。Gateway 是集中设定外部调用超时、启停问题服务的合适位置(例如在 Commerce API 出错时临时“切断”它)。
  • 最后在扩展与部署的话题里,我们会把 gateway 视作独立集群,可以对其做负载均衡、按 blue/green 与金丝雀方案发布,并在不影响内部 MCP 服务的情况下回滚。

本质上,如果你过去只想着“我有 App 和 MCP 服务器”,那么现在的图景会扩展为“我有 App、MCP Gateway、多个后端/Agents 集群以及 commerce backend”。而 gateway 的价值就在于:对 ChatGPT 来说仍然只有一个 MCP 入口。

11. 使用 MCP Gateway 时的常见错误

错误 1:把 gateway 变成“业务怪兽”。
常见陷阱:既然所有东西都要过 gateway,为什么不把折扣计算、SKU 选择、复杂类目规则或优惠码校验都放进来?最终你会得到一个臃肿、难以扩展与修改的服务,失去 Gift API、Commerce API 等专业组件的分层意义。更好的做法是把 gateway 保持为薄的网络层,把领域特定的逻辑留在各自的专业服务中。

错误 2:在 gateway 里保存长生命周期的用户状态。
“我们把用户的购物车直接存在 gateway 内存里吧”的想法在只有一个实例时看似不错。一旦有了第二个实例,麻烦就来了:真实的购物车在 A 还是 B?重启会怎样?Gateway 必须保持无状态:最多只保留少量握手或配置的缓存;会话与订单的状态要放在数据库或专门的服务里。

错误 3:让 ChatGPT 感知内部服务拓扑。
如果你开始把多个 API 服务器(Gift API、Commerce API 等)直接暴露给 ChatGPT,只在部分地方用 gateway,你就丢掉了最大的优势:单一入口与集中控制。一旦拓扑变化,你就得在多个地方改配置。更简单的办法是:把 MCP Gateway 设为 App 的官方 endpoint,并把所有内部变化都藏在它后面。

错误 4:在所有后端重复实现跨服务逻辑。
有些团队会在每个 REST 服务里分别实现认证、限流、日志与本地化。结果是 Gift API 一种权限与限流策略,Commerce API 又是另一种,App 行为变得不可预测。Gateway 就是用来集中做这些事的:校验令牌、确定租户与本地化、记录调用、应用限流,然后再去访问具体服务。

错误 5:在 gateway 中塞进重型计算与 LLM 调用。
从技术上说,你可以在 gateway 里再调用一个 LLM 模型、做复杂聚合或长时间的批处理。但这很快会把它变成另一个沉重的后端,既难以扩展也难以隔离。Gateway 应保持快速且可预测:最多做轻量转换与路由。重活留给 REST 服务或我们稍后会讲到的队列/worker。

错误 6:过早把基础设施复杂化。
另一种极端是:为了一个小教学 App,一开始就搭独立的 Kubernetes 集群、NGINX 栈、Cloudflare Workers 和一堆复杂配置。在没有真实负载与高可用需求前,这样做没有意义。完全可以从单一 MCP 单体或简单的 Node gateway 起步,再随着增长逐步把组件拆到集群和托管服务中。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION