MingJunDuan的博客
热爱可抵一切,探索未知之境
全站访问量

企业级 MCP 落地:网关、鉴权与多租户

你是公司基础平台的负责人。公司里有几十个内部系统——订单、用户、支付、风控、BI、客服,各业务线的 Agent 团队都想把这些能力接进去。三个月后你发现:同一套”查订单”逻辑被 5 个团队各写了一遍;数据库密码和第三方 AK 散落在每个团队的 Agent 配置里;一个实习生写的 Agent 把生产库查崩了,没人说得清是谁干的;合规来要访问日志,你交不出来。

上一篇《MCP 全面解析》讲的是”怎么接一个工具”——那是单机视角:一个 Host 连一个 Server,协议怎么握手、消息怎么流转。但企业落地时,真正的重头戏不是协议,而是一堆工具 + 一堆团队怎么被统一管起来

这里有一个当下很真实的争议:MCP 管了”工具调用”,但谁管”传输与治理”? MCP 规范本身只定义了”工具怎么描述、怎么发现、怎么调用”,至于注册发现、路由、鉴权、限流熔断、多租户隔离,规范一个字都没写——于是各家都在造自己的轮子,标准还没收敛。这就是”工具中台 / 聚合网关”要补的那一层。

这篇文章会用与之前 Dubbo、ZooKeeper、Apollo 系列相同的深度剖析风格,借 Apollo/网关的分布式经验,回答三个问题:

  1. 为什么:单机 MCP 在企业里会撞上什么墙,网关要解决什么。
  2. 怎么做:注册发现、路由、OAuth 2.1 鉴权、限流熔断、多租户隔离,五大能力如何落地。
  3. 代码:用 Spring AI + Spring Cloud Gateway + Spring Security + Resilience4j 给出关键实现代码。

每个治理能力,我都会先用一个真实企业的坑开场(匿名化,但都是互联网公司每天都在发生的事),再讲解法。


一、为什么单机 MCP 撑不起企业场景

1.1 从 N×M 到 N×M×T

上一篇里我们讲过 MCP 解决的是 N 个工具 × M 个模型的 N×M 集成困境:每个工具实现一次 MCP,任何客户端都能接。这个结论在单机完全成立。

但企业场景多了一个维度:T(Tenant,团队/租户)。真实世界里,公司内部的系统不是 3 个,是几十上百个;用这些能力的团队也不是 1 个,是每个业务线都有。

单机:      N 个工具 × M 个模型          → 解决"协议互通"
企业:      N 个内部系统 × M 个业务线 Agent × 每个团队各自直连  → 失控

单机视角里,你是”一个开发者接一个文件系统工具”;企业视角里,你是”平台方给十几个团队开放几十个内部系统”。前者是 USB-C 插头,后者是整个大楼的配电网。

1.2 单机直连的五个真实坑

如果放任每个团队自己写 MCP Server、自己直连后端,你会收获下面五类问题——它们不是假设,是互联网公司上 Agent 项目时几乎必踩的坑:

# 具体表现
1 凭据失控 DB 密码、第三方 AK/SK、内部接口 token 散落在各团队 Agent 配置里,无法统一轮换、无法吊销
2 权限失控 模型能调的边界没人统一收口,”最小权限”无从谈起;一个只该读数据的 Agent 拿到了写权限
3 流量失控 Agent 会”死循环”——模型误判反复调同一个工具,瞬间把下游服务打挂;且一个租户的失控流量会拖垮所有人
4 审计缺失 谁在什么时候调了什么工具、传了什么参数、改了哪些数据,没有记录,合规/排障两头都交不了差
5 重复建设 同一套系统能力被不同团队重复包装,工具清单碎片化、命名冲突、口径不一致(”查订单”有五个版本)

1.3 缺的不是协议,是控制面

这五类问题的共同点:MCP 协议解决不了它们,因为它们根本不在协议层。MCP 定义的是”工具怎么描述、怎么调用”,而这些问题属于”治理”——谁来注册、谁能调、调多少、调了怎么查。

这不陌生。回看我们熟悉的世界:

领域 “协议”管什么 “治理”管什么
RPC Dubbo 定义服务怎么调 注册中心(ZooKeeper/Nacos)管服务发现,网关管鉴权限流
微服务 HTTP/REST 定义接口怎么调 Service Mesh 管流量、可观测、安全
配置 Apollo 定义配置怎么读 Admin Service/Meta Server 管发布、发现、高可用
AI 工具 MCP 定义工具怎么调 ?——目前是空白,各家造轮子

结论一句话:MCP 生态现在是一堆 Server 裸奔,缺一层统一的控制面与数据面。补上这层的东西,业内叫它 MCP 网关,或更直白一点——工具中台


二、MCP 网关的整体架构

2.1 控制面与数据面分离

一个能落地的网关,第一件事是分清两条链路,别把它们糊在一起:

链路 干什么 频率 类比 Apollo
数据面 转发请求、执行鉴权、限流熔断、租户路由、审计 每个请求必经,高频 Config Service(配置读取)
控制面 工具注册、元数据管理、路由规则、配额策略、权限配置 低频管理操作 Portal + Admin Service(配置发布)

这个划分是 Apollo 给我们的最直接经验:读的链路和管的链路要分开,数据面必须无状态、可水平扩展,控制面才能做审批、版本、灰度这些”慢操作”。MCP 在 2026-07-28 版本走向无状态化,恰好让数据面网关可以像普通 HTTP 服务一样随便扩容——这是做网关的天时。

2.2 分层架构

                    ┌────────────────────────────┐
                    │   业务 Agent / Host(多租户)  │
                    │   租户A  租户B  租户C   …      │
                    └────────────┬───────────────┘
                         Streamable HTTP + OAuth 2.1
                    ┌────────────▼───────────────┐
                    │        MCP 网关(数据面)      │
                    │  ① 鉴权 → ② 路由 → ③ 限流熔断  │
                    │  → ④ 租户隔离 → ⑤ 审计         │
                    └────────────┬───────────────┘
                         内网转发(mTLS / 内网)
      ┌────────────────┬──────────────┴──────┬────────────────┐
      ▼                ▼                     ▼                ▼
 ┌─────────┐     ┌───────────┐          ┌─────────┐    ┌──────────┐
 │ MCP Server│    │ MCP Server│          │ 原生 API │    │  数据库/  │
 │ (订单)   │    │ (用户)   │          │(消息/缓存)│   │  第三方 SaaS│
 └─────────┘     └───────────┘          └─────────┘    └──────────┘
                    ▲
      ┌─────────────┴──────────────┐
      │  控制面:工具注册中心 / 路由规则  │
      │         / 租户策略 / 配额管理    │
      └─────────────────────────────┘

两个要点:

  1. 后端形态可以异构。网关后面接的不一定都是标准 MCP Server,也可以是还没改造的原生 API、数据库、第三方 SaaS——网关负责把”标准 MCP 请求”翻译/转发到”各种真实后端”。这让存量系统不用一上来就全面 MCP 化,落地阻力小很多。
  2. 网关是唯一的策略执行点。租户、权限、配额这些策略只在网关落地一次,后端 Server 不需要各自实现一遍治理逻辑——这正是”中台”的价值:把治理从 N 个后端里抽出来,收敛到一处。

2.3 网关要补的五大能力

后面五章逐一展开,先给一张总览:

能力 回答的问题 一句话定位
注册发现 有哪些工具?在哪?还活着吗? 工具从”硬编码”到”动态可发现”
路由 这个调用该打到哪个后端实例? 把”调哪个工具”翻译成”调哪个实例”
OAuth 2.1 鉴权 你是谁?你能调这个工具吗? 从”有 token”到”最小权限”
限流熔断 调用量失控、下游挂了怎么办? 保护下游,也保护模型自己
多租户隔离 租户之间会不会互相看见/互相影响? 企业落地的生死线

2.4 业界实证:控制面/数据面分离已被验证

这套”数据面 + 控制面”不是纸上谈兵,阿里系开源方案落地的就是同一张图:Higress 当 MCP Proxy(数据面),Nacos 当 MCP Registry(控制面)。Higress 负责流量转发与治理,Nacos 负责注册后端服务、维护 MCP 工具元数据(见《深度解析 Higress + Nacos 在 MCP Server 部署中的高可用、热更新与鉴权方案》)。


三、注册发现:工具从”硬编码”到”动态发现”

真实案例:某出行平台的 60+ 工具”没人管”。 平台部把订单、车辆、计价、地图等能力都包装成了 MCP Server,但没有统一登记。新 Agent 团队接入时,要一个个去 wiki 翻、去群里问”订单工具的内网地址是什么、参数格式谁定的”;更糟的是,某个工具下线了没人通知,一堆 Agent 还在傻傻地调,报错刷屏。

3.1 注册的粒度:不只注册 Server,要注册到”工具”

网关的注册中心,注册的最小粒度不是 Server,而是 Tool。原因很直接:后面所有的治理动作——路由、鉴权、限流——最终都落在单个工具上。

Server 级:  name=order-server, url=http://order.internal:8081, 协议版本, 健康状态
Tool 级:    name=query_order,  server=order-server,
            inputSchema={...}, 注解=[readOnlyHint],
            租户白名单={A,B},  限流=100 QPS,  scope=order:read

一个 MCP Server 会暴露多个工具,但不同工具的权限要求、限流阈值、危险程度完全不同——”查询订单”和”删除订单”绝不能享受同一套策略。所以注册表必须以工具为一行。

3.2 借 Apollo 的经验

Apollo 的注册发现我们拆过:Meta Server 封装 Eureka,Config/Admin Service 注册 + 心跳,客户端通过一个 HTTP 接口拿节点清单,永远不关心背后是谁。这套思想平移过来,MCP 网关需要:

Apollo 经验 映射到 MCP 网关
Meta Server 封装服务发现细节 网关对 Host 只暴露一个稳定入口,后端如何变化对调用方透明
服务实例注册 + 心跳保活 后端 MCP Server 注册 + 健康检查,心跳失败自动摘除
客户端拿到的是一份”节点清单” 网关维护一份”工具→实例清单”的注册表,缓存并定期刷新
配置变更推送(ReleaseMessage) 工具上下线时,通过 MCP 的 toolsListChanged 订阅主动推给网关刷新

更关键的是 2026-07-28 无状态化红利:MCP 去掉会话、每个请求自描述之后,后端 Server 天然无状态、可水平扩展,注册发现不再被”会话粘滞”绑架——这在工程上省了无数麻烦。

3.3 实现要点

一个最小可用的工具注册中心,核心是一张缓存表 + 一个发现动作:

// 工具注册表条目:工具 → 后端实例 → 治理元数据
public record ToolRegistration(
        String toolName,        // 全局唯一的工具名,如 query_order
        String serverName,      // 归属的后端 Server
        String baseUrl,         // 后端 Streamable HTTP 地址
        Map<String, Object> inputSchema,   // 入参 JSON Schema
        boolean readOnly,       // 由工具注解 readOnlyHint 推导
        Set<String> tenantAllowlist,       // 租户白名单
        int rateLimitQps,       // 工具级限流阈值
        String scope            // 工具级授权 scope,如 order:read
) {}

发现动作:网关作为 MCP 客户端去拉后端的 tools/list,把结果灌进注册表缓存:

@Service
public class ToolRegistryService {

    private final Cache<String, ToolRegistration> registry =
            Caffeine.newBuilder()
                    .maximumSize(10_000)
                    .expireAfterWrite(Duration.ofMinutes(5))
                    .build();

    // 用 Spring AI 的 MCP 客户端发现某个后端的工具清单,写入注册表
    public void discover(String serverName, String baseUrl, McpAsyncClient client) {
        List<Tool> tools = client.listTools().block().tools();
        for (Tool t : tools) {
            registry.put(t.name(), new ToolRegistration(
                    t.name(), serverName, baseUrl,
                    t.inputSchema() == null ? Map.of() : t.inputSchema().toMap(),
                    Boolean.TRUE.equals(t.annotations() == null ? null
                            : t.annotations().readOnlyHint()),
                    Set.of(), 0, serverName + ":" + t.name()));
        }
    }

    public ToolRegistration lookup(String toolName) {
        ToolRegistration r = registry.getIfPresent(toolName);
        if (r == null) {
            throw new ToolNotFoundException("未注册的工具: " + toolName);
        }
        return r;
    }
}

McpAsyncClient.listTools() 是 Spring AI MCP 客户端最常用的一个方法:它替你把”连上后端 → tools/list → 解析 Tool 列表”全做了。注册发现这个高频动作,一行调用就拿到后端能力清单。生产里这套逻辑会挂一个定时任务周期性重跑,配合 toolsListChanged 订阅做增量刷新。

3.4 业界实证:零代码扩展 Tool

Higress + Nacos 方案给出的承诺非常直白:”使用配置即可实现零代码扩展 Tool,新应用的注册、应用下面工具的扩展、工具 prompt 更新验证都能通过服务集成的可视化控制台,更新发布配置快速完成,接入方式极其简单”(出处)。注册发现的价值,就是把”接一个工具”从写代码降级成改配置。


四、路由:把”调哪个工具”变成”调到哪个实例”

真实案例:某电商大促的工具版本灰度。 订单查询工具要升一个大版本,入参 schema 变了(order_id 拆成 order_id + biz_type),老 Agent 团队还没改造完。如果一刀切切换,老团队全挂;必须按租户、按流量比例,把一部分流量灰度到新版本。

4.1 路由的输入与维度

路由要回答的问题是:请求进来了,打到哪个后端实例。它的输入不只有工具名,还有四个维度:

维度 例子 用途
工具名 query_order 最基本的归属路由:这个工具属于哪个后端
租户 tenant_id=A 租户分流:A 租户走专有集群、VIP 租户走高配实例
版本/灰度标签 v2gray 灰度发布:按比例把流量切到新版本 Server
机房/亲和 idc=华东1 就近路由:优先同机房,降低跨机房延迟(Apollo 同机房优先的思路)

4.2 网关是”策略点”,不是”转发器”

这里要纠正一个常见的理解偏差:MCP 网关不是简单反向代理,它是策略点。一个请求穿过网关时,鉴权、限流、熔断、审计全部挂在路由链上——先过策略,再决定放行到哪。

Host ──▶ 网关
          │  ① 鉴权(token → 租户 → 权限)
          │  ② 路由(工具名 + 租户 + 版本 → 目标实例)
          │  ③ 限流(租户级 + 工具级)
          │  ④ 熔断(下游健康?)
          │  ⑤ 审计(记录 + 脱敏)
          ▼
       转发到后端 Server

这五步构成一条过滤器链(Filter Chain),路由是其中一环,不是全部。下面用 Spring Cloud Gateway 落地。

4.3 实现要点

网关用 Spring Cloud Gateway 做数据面。路由规则可以直接写配置,按工具归属把请求分到不同后端:

spring:
  cloud:
    gateway:
      routes:
        - id: order-tools
          uri: http://localhost:8081          # 订单工具后端
          predicates:
            - Path=/mcp/order/**
        - id: user-tools
          uri: http://localhost:8082          # 用户工具后端
          predicates:
            - Path=/mcp/user/**

工具级、租户级的细粒度路由(比如灰度)则写进一个 GlobalFilter,从注册表里查目标:

@Component
public class ToolRouteFilter implements GlobalFilter, Ordered {

    private final ToolRegistryService registry;

    // 从请求里解出工具名 + 租户,决定目标后端;结果写进 exchange attribute 供后续转发
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String tool = extractToolName(exchange);      // 从 JSON-RPC body 里取 tools/call 的 name
        String tenant = extractTenant(exchange);      // 从 JWT claim 里取 tenant_id

        ToolRegistration reg = registry.lookup(tool);
        if (!reg.tenantAllowlist().isEmpty() && !reg.tenantAllowlist().contains(tenant)) {
            return unauthorized(exchange, "租户无权访问工具 " + tool);
        }

        // 记录路由决策,供后续过滤器使用(灰度标签、目标实例等)
        exchange.getAttributes().put("gw.tool", tool);
        exchange.getAttributes().put("gw.backend", reg.baseUrl());
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() { return 0; }
}

实际生产里,灰度会做成”配置中心下发路由表 + 网关热加载”:路由规则(谁、多少比例、切到哪个版本)放在 Apollo/Nacos 里,网关监听变更,无需重启。这也是 Apollo “配置发布 → 客户端热更新”那条链路在 MCP 网关上的复刻。

4.4 业界实证:接入点路径即路由

携程的网关里,路由的真实形态是”接入点路径”:为每个接入方(消费者)分配不同的接入点路径,一个路径关联多个模型路由,一个模型路由再关联多个后端服务做负载均衡(见《携程旅游的 AI 网关实践》)。这和本文”工具名 + 租户 + 版本 → 目标实例”的路由模型是同一个东西的工程化落地。


五、OAuth 2.1 鉴权:从”有 token”到”最小权限”

真实案例(一):实习生差点删库。 某公司给内测 Agent 配了个”全量工具包”,凭据(DB 读写账号)直接写在 Agent 的 MCP 配置文件里。实习生把这套配置拷去跑自己的实验,结果拿到了生产库的读写权限,一个误操作差点出事。

真实案例(二):只读 Agent 误调退款。 一个”客服查询助手”本应只能读订单,但因为鉴权只做到了”能不能连上 Server”,没做到”能不能调某个工具”,模型在某次对话里把”申请退款”这个工具也调了。

5.1 MCP 的鉴权现状与争议

MCP 规范在 2025-06-18 引入 OAuth 2.1,要求 Streamable HTTP Server 支持它。但规范解决的只是认证(Authentication)——”客户端是不是它声称的那个客户端、token 合不合法”,它不解决授权(Authorization)——”这个租户能不能调这个工具、能做读还是写”。授权是业务策略,必须由网关来收口。

这正好回答了开头那句”谁管传输与治理”的争议:认证是 MCP 规范给的标准,授权是网关给的治理,两者缺一不可。

5.2 认证层:OAuth 2.1 授权码 + 资源服务器校验

业务 Agent/Host                         网关(Resource Server)
     │                                      │
     │── ① 授权码流程 / 客户端凭证 → 授权服务器拿 access_token
     │<─ access_token(JWT,含 iss/aud/exp/scope/tenant_id)
     │                                      │
     │── ② 带 Authorization: Bearer <token> ──→│
     │                                      │── ③ 校验签名/iss/aud/exp/scope
     │<─ ④ 校验通过,进入路由 ────────────────│

几个校验点一个都不能少:

校验项 为什么 对应漏洞
签名/过期 token 是否由信任的授权服务器签发、是否过期 伪造 token、过期 token
iss(签发方) 防钓鱼:token 必须来自你配置的那台授权服务器 用别人家的授权服务器伪造签发方
aud(受众) token 是不是发给”这个网关”的 拿发给别的服务的 token 冒充
scope token 携带了哪些权限声明 权限越界

5.3 授权层:工具级 scope + 危险工具默认拒绝

认证过了只是”门开了”,授权才决定”你能碰哪些工具”。这里的核心思想是把 scope 细化到工具级,并借 MCP 的工具注解做安全兜底:

粗粒度:tenant_id=A 能访问 order-server(Server 级)
细粒度:tenant_id=A 对 query_order 有 order:read,对 cancel_order 无权限(Tool 级)
兜底:    cancel_order 声明了 destructiveHint → 即便有权限,也要人工确认

工具注解(readOnlyHint / idempotentHint / destructiveHint)是 MCP 协议送给网关的一份”安全元数据”——网关用它做默认拒绝:凡是 destructiveHint 的工具,一律要求额外的破坏性 scope + 人工确认。

5.4 实现要点

网关作为 OAuth 2.1 Resource Server,用 Spring Security 校验 JWT:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9000   # OIDC 发现定位授权服务器

工具级授权写成过滤器:从 JWT 的 scope 里查”这个工具需要的最小 scope 是否被授予”:

@Component
public class ToolAuthorizationFilter implements GlobalFilter, Ordered {

    private final ToolRegistryService registry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Authentication auth = exchange.getPrincipal()
                .cast(Authentication.class)
                .block();

        String tool = (String) exchange.getAttributes().get("gw.tool");
        ToolRegistration reg = registry.lookup(tool);

        boolean granted = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .anyMatch(a -> a.equals("SCOPE_" + reg.scope()));

        if (!granted) {
            return forbidden(exchange, "缺少工具 scope: " + reg.scope());
        }

        // 破坏性工具:要求额外的人工确认标记(Host 侧拦截)
        if (!reg.readOnly()) {
            exchange.getAttributes().put("gw.requiresConfirmation", true);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() { return 1; }
}

这就是案例二的正解:query_ordercancel_order 是两个不同 scope,只读 Agent 的 token 只带 order:readcancel_order 需要的 order:cancel 它没有,网关直接 403——模型连调它的机会都没有。

5.5 业界实证:Token 关联消费者 + 审批

携程的鉴权落地:访问方用 Bearer Token 认证,每个 Token 关联一个”消费者”,消费者能访问哪些服务需要申请和审批;后端服务的凭证统一存在网关里,消费者无感(出处)。有意思的是 Higress+Nacos 方案对”多 Provider 鉴权”的处理——推荐把鉴权信息透传给服务提供方自己完成,而不是把 AK 存网关,安全层面更稳妥(出处)。这两种都是真实存在的取舍:集中托管 vs 端到端透传,没有标准答案。


六、限流熔断:保护下游,也保护模型

真实案例:客服 Agent 把定价服务打挂。 某公司的智能客服 Agent,在一次模型幻觉里认定”重算价格”能回答用户问题,于是开始反复调用这个工具。由于没有限流,几十个并发把下游定价服务打挂了——而这套定价服务同时还在给真实订单出价。另一个租户的爬虫 Agent 也把共享的搜索服务打到熔断,殃及了其他所有租户。

6.1 为什么 MCP 网关必须有限流熔断

这里有个区别于传统 API 网关的关键点:传统限流防的是”人”,MCP 限流还要防”模型”。人是理智的(至少不会故意狂点),但模型会幻觉、会死循环、会在一轮 Agent Loop 里反复调同一个工具。一个失控的 Agent,就是一台天然的 DDoS 发射器。

所以 MCP 网关的限流,本质是给模型套一层”调用预算”——这是”治理”对”智能”的最后一道保险。

6.2 限流:租户级 + 工具级双层配额

层级 目的 例子
租户级 防止一个租户吃光所有资源,保证公平 每个租户全局 500 QPS / 并发 50
工具级 保护慢/贵/危险的下游 query_order 限 100 QPS;run_sql 限 5 QPS;send_sms 按租户限 1000/天

配额放配置中心(Apollo/Nacos),网关热加载——这样运营可以在大促前临时调高订单工具的配额,而不需要发版。

6.3 熔断与降级

限流挡的是”量”,熔断挡的是”下游已经不行了”。当下游 MCP Server 响应变慢、超时、错误率上升,网关应该快速失败,而不是让调用方(和模型)干等:

正常:  Host → 网关 → 后端(200ms 返回)
劣化:  后端错误率 > 50% / 超时率飙升
熔断:  Host → 网关 → 立即返回"工具暂不可用"(不碰后端,避免雪崩)
降级:  读类工具可返回缓存兜底结果;写类工具明确失败

对 Agent 而言,”快速失败”比”长时间卡住”好得多——模型拿到一个明确的错误信息,可以调整策略;而卡住只会让 Agent 无限重试、雪上加霜。

6.4 实现要点

用 Resilience4j 的注解把限流、熔断、兜底串起来:

@Service
public class ToolInvocationGuard {

    // 工具级限流:query_order 每秒 100 次
    @RateLimiter(name = "tool-query_order", fallbackMethod = "rateLimitedFallback")
    // 熔断:后端错误率/慢调用率超过阈值时打开
    @CircuitBreaker(name = "backend-order", fallbackMethod = "circuitOpenFallback")
    public Mono<String> invokeOrderTool(Mono<String> call) {
        return call; // 实际调用后端 MCP Server 的逻辑
    }

    public Mono<String> rateLimitedFallback(String tool, Throwable t) {
        return Mono.just("{\"error\": \"工具 " + tool + " 触发限流,请稍后重试\"}");
    }

    public Mono<String> circuitOpenFallback(String tool, Throwable t) {
        return Mono.just("{\"error\": \"工具 " + tool + " 暂不可用(下游熔断)\"}");
    }
}
resilience4j:
  ratelimiter:
    instances:
      tool-query_order:
        limit-for-period: 100
        limit-refresh-period: 1s
        timeout-duration: 0
  circuitbreaker:
    instances:
      backend-order:
        sliding-window-size: 20
        failure-rate-threshold: 50      # 错误率 50% 打开熔断
        wait-duration-in-open-state: 10s

这两层不要二选一,要叠着用:限流管”我主动控制你最多打多少”,熔断管”下游坏了我不再雪上加霜”。这也是我们在 Dubbo 治理里最熟的那套组合拳,只是保护对象从”服务”换成了”模型调用的工具”。

6.5 业界实证:TPM/QPM/并发三档限流 + 降级

携程的限流不是拍脑袋的一个 QPS,而是三档:Token per Minute(TPM)、Query per Minute(QPM)、并发请求数,消费者申请访问时就要填好阈值;实现上用 Redis 作中央计数器、Lua 脚本保证原子更新(出处)。降级也做实了:后端返回 4xx/5xx 时,网关自动把请求切到降级服务,而不是把错误原样抛给调用方。


七、多租户隔离:企业落地的生死线

真实案例(一):SaaS 平台越权。 某公司把 Agent 能力做成 SaaS 开放给外部客户,结果 A 客户通过构造 prompt 让模型”顺手查一下”别的租户数据,越权拿到了 B 客户的信息。

真实案例(二):财务 BU 调了 HR 工资工具。 某公司内部多个 BU 共用一个工具中台,因为工具清单对所有人一视同仁,财务 BU 的 Agent 竟然在列表里看到了 HR 的”查工资”工具,并且真的调成功了。

7.1 租户模型怎么定

租户(Tenant)是谁,取决于你的业务:内部多 BU,租户 = 业务线/团队;对外 SaaS,租户 = 客户。不管哪种,一个租户在网关里都由三元组定义:

租户 = { 命名空间(namespace) + 配额(quota) + 权限(permissions) }
  • 命名空间:租户的身份标识,贯穿所有日志与策略。
  • 配额:这个租户能吃多少资源(呼应上一章的双层限流)。
  • 权限:这个租户能调哪些工具、每种工具什么 scope。

7.2 四层隔离

多租户隔离不是一个开关,是四层都要做,缺一层就漏:

隔离什么 怎么落 防的坑
流量隔离 一个租户别打爆别人 路由按租户分流 + 限流按租户独立 爬虫租户拖垮全员
能力隔离 一个租户别看到别人的工具 tools/list 按租户过滤,看不见就调不了 财务 BU 看到 HR 工资工具
凭据隔离 一个租户别拿到别人的后端凭据 后端凭据由网关统一托管,按租户+工具绑定,Agent 拿不到明文 凭据满天飞、难轮换
数据隔离 一个租户别读到别人的数据 工具层强制 tenant_id 下推,跨租户访问直接拒绝 A 客户越权读 B 客户数据

其中能力隔离最容易被忽视,但它是最前置的一道闸:很多越权根本不是”调的时候拦”,而是”清单里就不该出现”。MCP 的 tools/list 是模型能看到全部能力的窗口,给不同租户返回不同工具清单,就等于给模型配了不同的”视野”。

这里的隔离粒度,直接借鉴 Apollo 的 AppId + Cluster + Namespace 思想:Apollo 用这三个维度把配置空间切得清清楚楚,MCP 网关用 租户 + 工具组 + scope 把能力空间切得清清楚楚。

7.3 实现要点

租户上下文贯穿整条过滤链,从 token 里解出来、存进 exchange attribute:

public record TenantContext(String tenantId, Set<String> scopes) {
    private static final String ATTR = "gw.tenant";

    public static TenantContext from(ServerWebExchange exchange) {
        Jwt jwt = (Jwt) exchange.getPrincipal().block();
        String tenantId = jwt.getClaimAsString("tenant_id");
        @SuppressWarnings("unchecked")
        Set<String> scopes = new HashSet<>(jwt.getClaimAsStringList("scope"));
        TenantContext ctx = new TenantContext(tenantId, scopes);
        exchange.getAttributes().put(ATTR, ctx);
        return ctx;
    }

    public static TenantContext current(ServerWebExchange exchange) {
        return (TenantContext) exchange.getAttributes().get(ATTR);
    }
}

能力隔离——给不同租户返回裁剪后的工具清单(聚合模式网关在 tools/list 响应里按租户过滤):

public List<Tool> visibleTools(String tenantId, List<Tool> allTools) {
    return allTools.stream()
            .filter(t -> {
                ToolRegistration reg = registry.lookup(t.name());
                return reg.tenantAllowlist().isEmpty()
                        || reg.tenantAllowlist().contains(tenantId);
            })
            .toList();
}

凭据隔离——后端凭据托管在网关的密钥库(Vault/KMS),按”租户+工具”维度绑定,转发时才注入:

// 网关转发前,从密钥库按 (tenantId, tool) 取出后端凭据注入请求头,Agent 全程无感、无明文
public Map<String, String> backendCredential(String tenantId, String tool) {
    String path = tenantId + "/" + tool;      // Vault 路径:租户/工具 维度的隔离
    return vaultClient.read(path);            // 返回后端 token/账号,仅网关可见
}

7.4 业界实证:命名空间即租户隔离

多租户隔离最干净的工业实现,是直接复用注册中心的命名空间能力:Higress + Nacos 方案里,”利用 Nacos 的命名空间能力做到服务和工具集的隔离,给不同的用户提供不同的 MCP 工具集“(出处)。这就是本文 7.2 “能力隔离”的现成解法——租户 A 连看都看不到租户 B 的工具。


八、可观测与审计:治理的最后一块拼图

真实案例:金融合规要 180 天可追溯。 某金融科技公司要过等保/审计,要求”谁在什么时间调用了什么工具、传了什么参数、返回了什么结果”可追溯 180 天,且入参里的手机号、身份证号等敏感字段必须脱敏。团队发现——前面做得再好,没有审计日志,合规这一关就是过不去。

8.1 全链路可观测

可观测要回答”这次调用发生了什么”,三个信号缺一不可:

信号 内容 手段
Trace 租户 → 网关 → 后端 → 耗时的完整链路 OpenTelemetry 埋点,traceId 贯穿
Metrics 各工具 QPS、错误率、P99 延迟、熔断状态 Prometheus + Grafana
Logs 每次调用的结构化日志(含 traceId 关联) 网关统一打点,后端不需要各打各的

8.2 审计日志:append-only + 脱敏

审计日志和普通日志是两回事:审计是合规证据,必须 append-only、不可篡改、可回放。一条审计记录至少包含:

{
  "timestamp": "2026-08-25T10:00:01.123Z",
  "tenantId": "bu-finance",
  "principal": "agent-42@bu-finance",
  "tool": "query_order",
  "arguments": "{\"order_id\": \"138****0012\"}",   // 敏感字段已脱敏
  "result": "{\"status\": \"ok\", \"rows\": 1}",
  "traceId": "a1b2c3...",
  "decision": "allowed",            // allowed / denied / rate_limited / circuit_open
  "latencyMs": 83
}

这里呼应一个我们在 DeepSeek Harness 架构里强调过的硬不变量——“模型可见 ⟺ 已记录(model-visible ⟺ logged)”。网关把它落地成:任何要放行给模型的结果,必然先落一条审计日志;反过来,任何能审计到的,都能回放出”模型当时看到了什么”。这一条同时解决了合规、排障、回放三件事。

8.3 一张治理矩阵收口

把全文五大能力 + 审计放进一张矩阵,作为网关的完整”职责清单”:

能力 防的问题 关键手段 借的分布式经验
注册发现 工具失控、死链 Tool 级注册表 + 心跳 + toolsListChanged Apollo Meta Server/Eureka
路由 打错实例、无法灰度 工具名+租户+版本多维度路由 Dubbo 服务路由、网关灰度
OAuth 2.1 鉴权 越权、凭据泄露 JWT 校验 + 工具级 scope + destructiveHint 默认拒绝 微服务统一认证
限流熔断 模型打爆下游 租户级+工具级限流 + Resilience4j 熔断降级 Dubbo 治理、Sentinel
多租户隔离 跨租户越权 流量/能力/凭据/数据四层隔离 Apollo AppId+Cluster+Namespace
审计 合规无据 append-only + 脱敏 + traceId 微服务可观测体系

8.4 业界实证:日志进 ClickHouse、指标进 Grafana

携程的可观测链路很典型:请求日志落本地磁盘 → logrotate 滚动 → FileBeat 送 Kafka → LogStash 消费解析 → 写 ClickHouse → Kibana 查询;监控则走网关暴露的 Prometheus 接口 → Grafana(出处)。这套东西复用公司现有监控链,不需要为 MCP 另起炉灶。


九、总结与展望

9.1 核心观点

回到开头那句争议:MCP 管了”工具调用”,但谁管”传输与治理”? 这篇文章的答案是——

单机看 MCP,企业看 MCP 网关。MCP 是”插座”,网关是”配电箱 + 电表 + 门禁”。

MCP 协议解决了”工具怎么统一接入”,而注册发现、路由、鉴权、限流熔断、多租户隔离、审计,这些治理能力协议一个字都没定义,必须由网关这一层补上。把治理收敛到网关一处,后端 Server 才能保持简单、无状态、专注做工具本身。

9.2 诚实的现状

需要坦白的是:目前没有一个官方标准 MCP 网关。MCP 规范还在演进,网关这一层各家都在自研——Spring AI 生态给的是 MCP Server/Client 的组件,阿里推 Higress,云厂商(AWS/Azure)推自己的 MCP 网关,KongAkkaTrueFoundry 等也在做各自的 MCP 访问控制与注册中心。方案大同小异——数据面代理 + 控制面注册中心——但生态远未收敛。这正是”谁管传输与治理”之争的当下状态:协议已定,治理未定

可以预见的收敛方向有三个:工具级授权(scope/注解)进入 MCP 规范、网关能力(限流/熔断/审计)形成最佳实践标准、MCP 网关与 Service Mesh / A2A 融合成统一的 Agent 基础设施。

9.3 最小落地清单

别一上来就上全量。企业落地 MCP 网关,按这个顺序起步,每一环都能独立见效:

  1. 一个网关 + 一个注册中心:先解决”工具散落、没人管”——把工具登记起来、统一入口。
  2. JWT 鉴权 + 工具级 scope:先解决”谁能调什么”——这是安全底线,最优先。
  3. 一个限流器:先解决”模型打爆下游”——用最简单的租户级限流挡住最痛的坑。
  4. 审计日志:先解决”出了事能查”——append-only 日志,成本低、价值高。
  5. 再逐步补:熔断降级 → 多租户隔离 → 灰度路由 → 可观测体系。

一句话总结

企业级 MCP 落地的关键,不是把协议用得多花哨,而是把”注册发现、路由、鉴权、限流熔断、多租户隔离”这五件事收敛到一个网关里——MCP 管”能不能调”,网关管”让不让调、调多少、调了怎么查”。

本文阅读量