MCP 网关、注册中心与代理:多服务器聚合与能力路由

MCP 网关、注册中心与代理实践:多服务器聚合、能力路由与命名空间隔离、服务注册发现与目录、鉴权与配额、协议转换、可观测性与高可用部署,帮助把散落的 MCP 服务器整合成统一入口。

1. 为什么需要 MCP 网关

当组织里只有一两个 MCP 服务器时,客户端直接连就行。当服务器涨到几十个、上百个,问题就变了:客户端要维护一堆连接配置、工具名互相冲突、鉴权各自为政、故障排查无从下手。MCP 网关(gateway)就是把这些复杂性收敛到一个统一入口。

1.1 直连模式的三个痛点

# 痛点一: 客户端配置爆炸
#   每个 IDE/Agent 都要写 N 份 server 配置,改一次要改 N 处
# 痛点二: 命名冲突与能力发现
#   两个服务器都有 search 工具,客户端如何区分、如何选择
# 痛点三: 横切关注点无处安放
#   鉴权、配额、审计、限流、灰度——每个服务器重复实现
# 网关 = 把横切关注点从 N 个服务器收敛到 1 个入口

1.2 网关的职责边界

职责网关做服务器做
鉴权统一 OAuth/令牌校验业务级授权
路由工具名 → 目标服务器工具实现
配额按主体/工具限流内部资源调度
审计全量调用日志业务日志
聚合合并 tools/list各自能力声明
转换stdio ↔ HTTP无

一句话:网关不是「多一跳」,而是「把 N 份重复的横切逻辑换成 1 份统一策略」。

2. 多服务器聚合与命名空间

聚合的第一个问题是命名:不同服务器可能导出同名工具。网关必须建立一套命名空间规则,否则模型看到两个 search 会无所适从。

2.1 命名空间策略

策略形式优点缺点
前缀式github__create_pr无歧义、易路由名字变长、token 略增
后缀式create_pr@github可读部分客户端不支持特殊字符
扁平 + 冲突拒绝冲突时报错名字干净需人工干预
分域按会话只挂载一个域上下文干净切换成本

2.2 聚合 tools/list

// 网关把多个后端的 tools/list 合并成一个
async function aggregateTools(): Promise<Tool[]> {
  const backends = registry.list();
  const merged: Tool[] = [];
  for (const b of backends) {
    const tools = await b.client.listTools();
    for (const t of tools) {
      merged.push({
        ...t,
        // 前缀式命名空间:<server>__<tool>
        name: `${b.namespace}__${t.name}`,
        // 保留原始名,便于回程路由
        _origin: { server: b.id, tool: t.name },
        // 注入配额/审计提示,模型可见
        description: `[${b.namespace}] ${t.description ?? ""}`,
      });
    }
  }
  return merged;
}

2.3 回程路由

// 收到 tools/call 后,从命名空间解析目标服务器
async function routeCall(name: string, args: unknown, ctx: CallContext) {
  const sep = name.indexOf("__");
  if (sep < 0) throw new McpError(-32601, `unknown tool: ${name}`);
  const ns = name.slice(0, sep);
  const tool = name.slice(sep + 2);

  const backend = registry.get(ns);
  if (!backend) throw new McpError(-32601, `unknown namespace: ${ns}`);
  await quota.check(ctx.principal, backend.id);
  const res = await backend.client.callTool({ name: tool, arguments: args });
  audit.record({ principal: ctx.principal, ns, tool, args, res });
  return res;
}

一句话:命名空间既是「防冲突」的手段,也是「可路由」的关键——名字里必须携带回程信息。

3. 能力路由

除了「按名字路由」,网关还能做更聪明的路由:按能力标签、按权限、按负载、按版本。

3.1 路由维度

# 1) 精确名路由: 工具名直接映射(最常用)
# 2) 标签路由: 工具带 tags(read/write/db/cloud),按标签批量启用
# 3) 权限路由: 不同主体看到不同工具子集(RBAC)
# 4) 版本路由: v1/v2 并存,灰度切换
# 5) 负载路由: 同一能力的多个副本间做负载均衡
# 6) 地理位置路由: 就近接入降低延迟

3.2 权限过滤的 tools/list

// 按主体权限裁剪可见工具:模型根本看不到无权使用的工具
async function listToolsFor(principal: Principal): Promise<Tool[]> {
  const all = await aggregateTools();
  return all.filter((t) => {
    const policy = policies.for(principal, t._origin);
    return policy.canSee;   // 不可见 = 不出现在列表里
  });
}

3.3 路由表配置

# gateway routes 配置
namespaces:
  github:
    transport: streamable-http
    url: https://mcp.internal/github
    tools: ["list_prs", "create_pr", "get_issue"]
    tags: [vcs, write]
  k8s:
    transport: streamable-http
    url: https://mcp.internal/k8s
    tags: [cloud, write]
  docs:
    transport: stdio
    command: ["mcp-docs", "--root", "/srv/docs"]
    tags: [read]
routing:
  strategy: namespace-prefix
  deny_untagged_write: true      # 未显式授权的写工具一律不可见

4. 注册中心与目录

网关怎么知道有哪些服务器?答案是注册中心(registry):一张「谁提供什么能力、在哪里、健康与否」的目录。

4.1 注册信息模型

字段含义示例
id / namespace唯一标识github
endpoint接入地址https://mcp.internal/github
transport传输方式streamable-http / stdio
capabilities声明的能力tools, resources, prompts
health健康状态healthy / degraded / down
owner归属团队platform-team
version版本1.4.2

4.2 服务发现流程

# 1) 服务器启动 → 向注册中心注册(或注册中心主动探测)
# 2) 网关拉取目录 → 建立本地路由表(带缓存)
# 3) 定期健康检查 → 更新 health 状态
# 4) 客户端请求 → 网关按路由表转发
# 5) 服务器下线 → 注销或健康检查失败 → 从路由表摘除
# 目录是"能力的真相来源",网关只是它的消费者

4.3 注册 API 示例

# 服务器向注册中心注册
curl -X POST https://registry.internal/v1/servers \
  -H "Authorization: Bearer $REGISTRY_TOKEN" \
  -d '{
    "namespace": "github",
    "endpoint": "https://mcp.internal/github",
    "transport": "streamable-http",
    "capabilities": ["tools"],
    "owner": "platform-team",
    "version": "1.4.2",
    "ttl_seconds": 30
  }'

一句话:注册中心解决「有哪些能力」,网关解决「怎么到达它们」——两者解耦,才能各自独立演进。

5. 鉴权与配额

网关是天然的鉴权收口点:客户端只跟网关建立信任,后端服务器只信任网关。

5.1 鉴权模型

# 1) 客户端 → 网关: OAuth 2.1 令牌(含 scope)
# 2) 网关 → 后端: 服务身份(mTLS 或内部令牌)
# 3) 网关校验 scope 与目标工具所需权限是否匹配
# 4) 后端不再直接面对用户,只信任网关的服务身份
# 关键: 用户身份必须"透传"给后端(用于后端自己的授权与审计)

5.2 令牌透传与降权

// 网关把用户身份转为后端可验证的签名头
function forwardHeaders(ctx: CallContext): Record<string, string> {
  return {
    // 服务身份(网关自己)
    "x-mcp-gateway": "gw-prod-01",
    // 用户身份(签名,防伪造)
    "x-mcp-principal": ctx.principal.id,
    "x-mcp-scopes": ctx.scopes.join(" "),
    "x-mcp-signature": sign(ctx.principal.id + ctx.scopes, GW_PRIVATE_KEY),
    // 追踪
    "x-request-id": ctx.requestId,
  };
}

5.3 配额维度

维度示例超限动作
主体每用户 1000 次/天拒绝 + 告警
工具create_pr 20 次/小时拒绝
命名空间github 组 5000 次/天拒绝
并发每主体 5 并发排队
令牌单次响应 256 KB截断
成本云工具累计花费熔断
# 令牌桶限流(按主体 + 工具)
class TokenBucket:
    def __init__(self, rate: float, burst: int):
        self.rate, self.burst = rate, burst
        self.tokens, self.ts = burst, time.monotonic()

    def allow(self, cost: float = 1.0) -> bool:
        now = time.monotonic()
        self.tokens = min(self.burst, self.tokens + (now - self.ts) * self.rate)
        self.ts = now
        if self.tokens >= cost:
            self.tokens -= cost
            return True
        return False

一句话:鉴权管「能不能」,配额管「能多少」——两者都在网关收口,才能对全组织生效。

6. 协议转换

网关常常要弥合异构:有的后端是 stdio 本地进程,有的是 Streamable HTTP 远程服务,客户端可能只支持其中一种。

6.1 转换矩阵

客户端 →stdio 后端HTTP 后端
stdio 客户端透传HTTP → stdio 桥
HTTP 客户端stdio → HTTP 桥透传/负载均衡

6.2 stdio 到 HTTP 的桥接

// 把本地 stdio 服务器的 JSON-RPC 转发为网关内部的调用
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "mcp-docs",
  args: ["--root", "/srv/docs"],
  stderr: "pipe",       // 后端日志走 stderr,别污染 JSON-RPC 通道
});
const client = new Client({ name: "gw-bridge", version: "1.0.0" });
await client.connect(transport);

6.3 JSON-RPC 透传的注意点

# 1) 保留原始 id 映射: 网关自己的 id 与后端 id 要建立映射表
# 2) 能力协商在连接建立时完成: initialize 握手各自独立
# 3) 通知(notifications)要双向转发: tools/list_changed 等
# 4) 错误码透传: -32601/-32602 等标准码不要被网关改写
# 5) 大响应分块: HTTP 侧要处理流式与截断
# 网关是"协议透明"的,改协议细节会破坏客户端的兼容假设

7. 可观测性

网关是全局唯一能看到「所有工具调用」的位置,天然是观测的最佳埋点。

7.1 关键指标

# 1) 调用量: 按主体/命名空间/工具/状态
# 2) 延迟: P50/P95/P99,区分网关自身开销与后端耗时
# 3) 错误率: 按错误码分类(协议错/鉴权错/后端错/超时)
# 4) 配额命中: 限流触发次数
# 5) 后端健康: 各服务器可用率、连接复用率
# 6) 工具热度: 哪些工具被高频调用、哪些从没被用过

7.2 分布式追踪

{
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span": "tools/call",
  "name": "github__create_pr",
  "attributes": {
    "mcp.principal": "user:alice",
    "mcp.namespace": "github",
    "mcp.tool": "create_pr",
    "mcp.backend_latency_ms": 412,
    "mcp.gateway_latency_ms": 18,
    "mcp.status": "ok"
  }
}

一句话:网关的追踪要能区分「网关自身耗时」与「后端耗时」,否则优化无从下手。

8. 高可用部署

网关成了单点,就必须按「它一定会挂」来设计。

8.1 高可用要点

关注点做法
无状态会话状态外置到 Redis/DB,网关实例可随意扩缩
多副本至少 3 副本,跨可用区
健康检查主动探测后端 + 自身就绪探针
熔断降级后端连续失败则快速失败,避免雪崩
连接复用后端连接池化,避免每请求重建
优雅退出摘流 → 等待在途请求完成 → 关闭

8.2 熔断器

class CircuitBreaker {
  private failures = 0;
  private state: "closed" | "open" | "half" = "closed";
  private openedAt = 0;

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.openedAt > 30_000) this.state = "half";
      else throw new McpError(-32000, "backend circuit open");
    }
    try {
      const r = await fn();
      this.failures = 0;
      this.state = "closed";
      return r;
    } catch (e) {
      if (++this.failures >= 5) {
        this.state = "open";
        this.openedAt = Date.now();
      }
      throw e;
    }
  }
}

8.3 部署拓扑

# 客户端(IDE/Agent)
#      │  HTTPS + OAuth
#      ▼
#  [ LB / Ingress ]
#      │
#      ▼
#  [ MCP 网关 x3 ]  ←→  [ Redis: 会话/配额 ]  ←→  [ 注册中心 ]
#      │
#      ├── HTTP ──→ [ github-mcp ]  [ k8s-mcp ]
#      └── stdio ─→ [ docs-mcp (sidecar) ]
# 网关无状态、后端可异构、注册中心是真相来源

9. 从直连迁移到网关

已有直连配置的团队,迁移要平滑,不能一刀切。

9.1 迁移步骤

# 1) 网关旁路部署: 只做观测,不改变流量(影子模式)
# 2) 单个服务器试点: 把一个服务器接入网关,客户端改为连网关
# 3) 验证: 对比直连与网关的工具列表、调用结果一致性
# 4) 批量迁移: 按命名空间逐步迁移
# 5) 收敛: 直连配置退役,客户端只剩网关一个入口
# 影子模式先看清流量,再动真格

9.2 兼容性检查清单

# 迁移前确认
# 1) 客户端支持命名空间前缀吗(名字变长)
# 2) 客户端支持 Streamable HTTP 吗(原为 stdio)
# 3) 后端的能力协商字段是否被网关完整透传
# 4) notifications 是否双向可达
# 5) 大响应/流式是否被网关缓冲破坏

10. 常见陷阱

  • 命名空间用特殊字符:@、: 部分客户端不支持——用 __ 这类安全分隔符。
  • 网关有状态:会话存在网关本地内存,扩容后会话丢失——状态外置。
  • 鉴权只做认证不做授权:任何登录用户都能调 create_pr——scope 与工具权限绑定。
  • 配额只按用户:某个用户建了一堆子账号绕开配额——按主体 + 工具 + 命名空间多维限流。
  • 无熔断:一个后端挂了拖垮整个网关——熔断 + 快速失败。
  • 吞掉错误码:网关把后端的 -32602 改写成通用错误——透传标准错误码。
  • 转发 stderr 到 stdout:stdio 后端的日志污染 JSON-RPC——日志走 stderr。
  • 没有影子期:直接切流量,出问题回滚困难——先旁路观测再迁移。

11. 总结

MCP 网关、注册中心与代理,解决的是「服务器数量增长后」的规模化问题:把散落的连接收敛为统一入口,把重复的横切逻辑收敛为统一策略。四个核心构件是:聚合与命名空间(合并能力、前缀路由、回程可解析)、注册与发现(目录作为能力真相来源,网关作为消费者)、鉴权与配额(认证在网关、身份透传、多维限流)、可观测与高可用(全链路追踪、无状态多副本、熔断降级)。落地节奏上,先影子观测、再单点试点、后批量迁移,比一次性切换安全得多。与 https://plumephp.com/mcp-multi-server-orchestration/ 的编排视角、https://plumephp.com/mcp-production-deployment/ 的部署实践、https://plumephp.com/mcp-remote-streamable-http/ 的传输细节配合阅读,能覆盖从单机到集群的完整路径。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 服务器评估与基准测试:工具选择、参数填充与任务成功率
  2. MCP 云基础设施与 IaC 工具:plan/apply 分离与爆炸半径控制
  3. MCP Git 与 DevOps 工具服务器:从只读查询到 CI/CD 触发