A2A 与 MCP 双协议网关设计指南
📍 系列定位 :本篇是 A2A 系列的"番外篇",承接第 5 篇(A2A + MCP 双协议)和第 6 篇(企业级网关治理),专门回答一个落地时最容易纠结的架构问题:A2A 和 MCP 在同一个 project 里,网关到底该怎么管?
先给结论:
完全可以,而且很常见。但"网关怎么处理"取决于你把 MCP 和 A2A 放在什么位置------是"对外暴露的两个面",还是"一个对内、一个对外"。
一、先分清三种形态
| 形态 | 结构 | 网关需求 |
|---|---|---|
| A. 并列暴露 | 一个 project,MCP Server module + A2A Server module,都对外 | 需要同时处理两种协议 |
| B. 双端点 | 一个 module 同时暴露 MCP 和 A2A 端点 | 同一进程,网关按路径分流 |
| C. 内外分工(最常见) | Agent module 对外是 A2A Server,对内是 MCP Client | 网关只管 A2A,MCP 走内网 |
大多数生产系统是 C:Agent 对外用 A2A 被委派,对内用 MCP 调工具。MCP Server 是内部基础设施,不对外暴露,所以不需要"对外网关同时处理两种协议"。
只有 A 和 B 才需要认真设计"双协议网关"。
💡 架构原则:先判断"MCP 是否需要被外部调用方直接访问",再决定网关复杂度。不要一上来就设计双协议网关,那是过度设计。
二、形态 A:多 module 并列
项目结构
text
agent-platform/
├── pom.xml
├── mcp-common/ # 公共:日志、鉴权工具、traceId
├── mcp-sales-server/ # MCP Server,暴露 sales_query
│ └── endpoint: /mcp/sales
├── mcp-hr-server/ # MCP Server,暴露 hr_query
│ └── endpoint: /mcp/hr
├── a2a-sales-agent/ # A2A Server,skill: sales_analysis
│ └── endpoint: /a2a/sales
├── a2a-orchestrator/ # A2A Server,编排其他 Agent
│ └── endpoint: /a2a/orchestrator
└── gateway/ # 统一网关
对外只暴露一个域名:
text
https://api.example.com/mcp/sales → mcp-sales-server
https://api.example.com/mcp/hr → mcp-hr-server
https://api.example.com/a2a/sales → a2a-sales-agent
https://api.example.com/a2a/orchestrator → a2a-orchestrator
网关方案:统一网关,按路径分流
text
Client
│ HTTPS
▼
┌─────────────────────────────────────┐
│ 统一网关 │
│ 1. TLS 终止 │
│ 2. 按路径识别协议 │
│ /mcp/** → MCP 处理链 │
│ /a2a/** → A2A 处理链 │
│ 3. 统一鉴权(OAuth2 / Bearer) │
│ 4. 统一 traceId │
│ 5. 分协议限流、审计 │
└─────────────────────────────────────┘
│ │
▼ ▼
MCP Servers A2A Agents
关键:协议识别靠路径前缀。 这是最简单、最可靠的方式。不要靠解析 JSON-RPC body 来识别协议------那是兜底手段,不是首选。
网关 Filter 骨架
java
// 基于 Spring Cloud Gateway (WebFlux) 的 GlobalFilter 骨架。
// 如果使用 Servlet 栈(Spring MVC),需替换为 HandlerInterceptor 或 Servlet Filter。
public class ProtocolRoutingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
String traceId = UUID.randomUUID().toString();
exchange.getAttributes().put("traceId", traceId);
if (path.startsWith("/mcp/")) {
exchange.getAttributes().put("protocol", "MCP");
return handleMcp(exchange, chain);
}
if (path.startsWith("/a2a/")) {
exchange.getAttributes().put("protocol", "A2A");
return handleA2a(exchange, chain);
}
return reject(exchange, 404, "未知路径:" + path);
}
private Mono<Void> handleMcp(ServerWebExchange exchange, GatewayFilterChain chain) {
// MCP 特有:SSE 长连接、Tool 级限流
return auth(exchange)
.then(rateLimit(exchange, "MCP"))
.then(chain.filter(exchange))
.doFinally(s -> audit(exchange, "MCP"));
}
private Mono<Void> handleA2a(ServerWebExchange exchange, GatewayFilterChain chain) {
// A2A 特有:委派链防循环、Task 级审计、回调鉴权
return auth(exchange)
.then(antiLoop(exchange)) // X-A2A-Chain 检查
.then(rateLimit(exchange, "A2A"))
.then(chain.filter(exchange))
.doFinally(s -> audit(exchange, "A2A"));
}
}
两种协议的网关职责差异
| 职责 | MCP 处理链 | A2A 处理链 |
|---|---|---|
| 鉴权 | OAuth2 scope 映射 Tool | OAuth2 scope 映射 Skill |
| 限流 | 按 Tool 计数 | 按 Agent + Skill 计数 |
| 会话 | SSE 长连接、粘滞 | SSE + Task 状态 |
| 防循环 | 不需要 | 必须 (X-A2A-Chain) |
| 审计 | Tool 名 + 参数摘要 | 委派链 + Task 状态变化 |
| 回调 | 无 | 推送通知回调鉴权 |
| 缓存 | Tool 列表可缓存 | Agent Card 可缓存 |
三、形态 B:一个 module 双端点
结构
text
agent-service/
├── src/main/java/
│ ├── McpEndpoints.java // /mcp 端点,暴露工具
│ ├── A2aEndpoints.java // /a2a 端点,暴露 Agent
│ └── SharedBusinessService.java
└── application.yml
yaml
server:
port: 8080
servlet:
context-path: /agent
# 端点:
# https://api.example.com/agent/mcp
# https://api.example.com/agent/a2a
网关处理
和形态 A 一样,按路径分流:
text
/agent/mcp/** → MCP 处理链
/agent/a2a/** → A2A 处理链
区别只是下游是同一个服务。网关不关心下游是不是同一个进程,只关心路径对应的协议。
⚠️ 注意 :同一个进程里,MCP 的 SSE 和 A2A 的 SSE 可能共享线程池和连接数。网关的限流要分协议独立计数,否则一个协议的流量会挤占另一个。
四、形态 C:内外分工(推荐)
结构
text
sales-agent/
├── a2a/ # 对外:A2A Server
│ ├── AgentCard.java
│ └── SalesA2AHandler.java
├── mcp/ # 对内:MCP Client
│ └── McpClientHolder.java
└── service/
└── SalesService.java
text
外部 Agent ──A2A──> 网关 ──> sales-agent ──MCP──> 内网 MCP Server
网关只处理 A2A
- 对外只暴露
/a2a/**。 - MCP Client 调内网 MCP Server,不经过对外网关,走内网服务发现或直连。
- 对外网关不需要懂 MCP 协议。
这是最省心的形态:
- 网关职责单一,只处理 A2A。
- MCP 在内网,安全边界由内网策略控制。
- 两种协议的生命周期解耦。
🚨 内网不等于裸奔:内网 MCP Server 仍需校验调用方身份,防止内网其他被攻陷的服务越权调用工具。常见手段:
手段 说明 适合 mTLS 服务间双向证书认证,调用方必须是授权服务 高安全要求、服务网格环境 内部 JWT 内部 CA 签发短时 token,MCP Server 校验签名和 scope 通用,易与现有 OAuth2 体系对接 K8s ServiceAccount Token Pod 身份,结合 NetworkPolicy 使用 云原生环境 NetworkPolicy 网络层限制只有指定 namespace/pod 能访问 MCP Server 兜底层,不能替代应用层鉴权 原则:对外网关是"第一道门",内网鉴权是"第二道门"。两道门都要有,不能只靠一道。
💡 判断标准:MCP Server 是否需要被外部调用方直接访问?
- 不需要 → 形态 C,网关只管 A2A。
- 需要 → 形态 A/B,网关要处理双协议。
五、双协议网关的四个关键设计
1. 协议识别:路径优先,方法名兜底
java
// 首选:路径前缀
if (path.startsWith("/mcp/")) protocol = "MCP";
if (path.startsWith("/a2a/")) protocol = "A2A";
// 兜底:解析 JSON-RPC method
// MCP: tools/list, tools/call, resources/read, prompts/get
// A2A: message/send, message/stream, tasks/get, tasks/cancel
不要只靠方法名,因为:
- 解析 body 有性能开销
- 流式请求 body 可能不完整
- 方法名可能重叠(未来协议演进)
路径前缀是契约,方法名是辅助。
2. 鉴权:统一入口,分协议授权
text
OAuth2 token
├── scope: mcp.sales.read → 允许调 /mcp/sales 的只读 Tool
├── scope: a2a.sales.invoke → 允许委派 /a2a/sales
└── scope: a2a.orchestrator → 允许调编排 Agent
网关统一校验 token,按 scope 决定:
- 这个 token 能访问哪些
/mcp/* - 这个 token 能委派哪些
/a2a/*
不要让 MCP Server 和 A2A Agent 各自解析 token,否则鉴权逻辑分散,审计也难统一。
3. 审计:统一 traceId,分协议字段
json
{
"traceId": "trace-abc",
"protocol": "A2A",
"caller": "agent-b",
"target": "sales-agent",
"skill": "sales_analysis",
"chain": ["orchestrator", "sales-agent"],
"taskState": "completed",
"cost": 1200
}
json
{
"traceId": "trace-def",
"protocol": "MCP",
"caller": "sales-agent",
"target": "mcp-sales-server",
"tool": "sales_query",
"args": {"region": "华东", "week": "2026-W20"},
"status": "ok",
"cost": 35
}
同一个 traceId 贯穿 A2A 委派和内部 MCP 调用,才能看到完整链路。
4. 发现:统一目录或 Agent Card 代理
💡 问题:如果 MCP 和 A2A 都对外暴露,Client 怎么知道哪些路径是 MCP,哪些是 A2A?
三种做法:
| 做法 | 说明 | 适合 |
|---|---|---|
| A2A Agent Card 扩展 | 在 Agent Card 的 extensions 字段声明关联的 MCP 端点 |
以 A2A 为主入口 |
网关统一 /registry |
网关提供 /registry 接口,返回所有可用 Agent 和 MCP Server 列表 |
双协议平等,需要统一目录 |
| 静态文档 | README / 开发者门户列出所有端点 | 小规模,端点少 |
Agent Card 扩展示例:
json
{
"name": "sales-agent",
"url": "https://api.example.com/a2a/sales",
"extensions": {
"mcp_endpoints": [
{ "name": "sales_query", "url": "https://api.example.com/mcp/sales" }
]
}
}
网关统一 /registry 示例:
json
GET https://api.example.com/registry
{
"a2a": [
{ "name": "sales-agent", "url": "/a2a/sales", "skills": ["sales_analysis"] },
{ "name": "orchestrator", "url": "/a2a/orchestrator", "skills": ["multi_agent"] }
],
"mcp": [
{ "name": "sales-server", "url": "/mcp/sales", "tools": ["sales_query"] },
{ "name": "hr-server", "url": "/mcp/hr", "tools": ["hr_query"] }
]
}
⚠️
/registry要鉴权:不是所有调用方都能看到全部端点,按 token scope 过滤。
六、推荐架构
小规模(≤3 个 MCP Server,≤3 个 A2A Agent)
text
Client
│
▼
Nginx(TLS + 路径路由 + 基础限流)
├── /mcp/** → MCP Servers
└── /a2a/** → A2A Agents
Nginx 做粗粒度路由和 TLS,鉴权在应用层做。简单、够用。
中规模(多团队,需要统一鉴权和审计)
text
Client
│
▼
统一网关(Spring Cloud Gateway / APISIX)
├── 协议识别(路径)
├── OAuth2 鉴权(scope 分协议)
├── 分协议限流
├── 统一 traceId + 审计
├── A2A 防循环
└── 路由
├── /mcp/** → MCP Servers
└── /a2a/** → A2A Agents
大规模(企业级)
text
Client
│
▼
L7 负载均衡 / CDN
│
▼
统一网关(控制面 + 数据面分离)
├── 控制面:权限矩阵、限流策略、灰度规则
└── 数据面:协议识别、鉴权、限流、审计、防循环
├── /mcp/** → MCP 服务集群(Nacos 发现)
└── /a2a/** → A2A Agent 集群(Registry 发现)
七、常见坑
| 坑 | 现象 | 解决 |
|---|---|---|
| 只按方法名识别协议 | 流式 body 不完整时识别失败 | 路径前缀优先 |
| MCP 和 A2A 共享限流计数器 | 一个协议流量挤占另一个 | 分协议独立计数 |
| MCP SSE 和 A2A SSE 共享连接池 | 长连接互相影响 | 分连接池或分实例 |
| HTTP/2 下的 SSE 队头阻塞 | MCP 和 A2A 的 SSE 流在同一个 HTTP/2 连接上多路复用,一个流阻塞会导致其他流(甚至普通 JSON-RPC 请求)被卡住 | 网关对 /mcp/** 和 /a2a/** 的 SSE 端点强制降级为 HTTP/1.1,或确保网关到下游使用独立的 HTTP/1.1 连接池 |
| 鉴权逻辑分散在各 module | 审计难统一,权限难收敛 | 网关统一鉴权,下游只信任身份头 |
| A2A 防循环只做在 Agent 层 | 恶意 Agent 绕过 | 网关层也要检查 X-A2A-Chain |
| MCP 工具对内网暴露 | 绕过 A2A 鉴权直接调工具 | MCP 只在内网,不经过对外网关;内网也要鉴权(mTLS / 内部 JWT / K8s SA Token) |
| traceId 不贯穿 | A2A 委派和 MCP 调用日志对不上 | 网关生成 traceId,透传到下游 |
| 双协议共享线程池 | 一个协议阻塞拖垮另一个 | 分线程池或分实例部署 |
/registry 未鉴权 |
端点信息泄露 | 按 token scope 过滤返回内容 |
关于 HTTP/2 SSE 队头阻塞的补充
🚨 这是双协议网关最隐蔽的 P0 级坑。
HTTP/2 多路复用让多个流共享一个 TCP 连接。SSE 是长连接,如果某个流的 TCP 包丢失,整个 HTTP/2 连接上的所有流都会被阻塞(TCP 层队头阻塞,HTTP/2 无法绕过)。
场景:
text
Client ──HTTP/2──> Gateway ──HTTP/2──> MCP Server(SSE)
└──HTTP/2──> A2A Agent(SSE)
如果 MCP 的 SSE 流因为网络抖动丢包,A2A 的 SSE 流也会被卡住,甚至普通 JSON-RPC 请求也会超时。
解决:
| 方案 | 说明 |
|---|---|
| SSE 端点强制 HTTP/1.1 | 网关对 /mcp/** 和 /a2a/** 的 SSE 路径,下游连接降级为 HTTP/1.1 |
| 独立连接池 | 网关到 MCP 和网关到 A2A 用不同的 HTTP/1.1 连接池 |
| 分实例部署 | MCP 和 A2A 部署在不同进程/集群,物理隔离 |
Nginx 示例:
nginx
location /mcp/ {
proxy_pass http://mcp_backend;
proxy_http_version 1.1; # 强制 HTTP/1.1
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /a2a/ {
proxy_pass http://a2a_backend;
proxy_http_version 1.1; # 强制 HTTP/1.1
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 3600s;
}
八、一句话总结
A2A 和 MCP 可以放在同一个 project 的不同 module,甚至同一个 module。
网关处理的关键不是"能不能同时处理两种协议",而是"先判断 MCP 是否对外"------对外就双协议网关按路径分流,不对外就网关只管 A2A,MCP 走内网并做内网鉴权。
如果做双协议网关:路径识别协议、OAuth2 分 scope 鉴权、分协议限流、统一 traceId 审计、A2A 侧加防循环、SSE 端点防 HTTP/2 队头阻塞。
常见场景:
- "MCP 要对外给第三方开发者调用" → 需要 Nginx / APISIX 双协议配置骨架(含 SSE 代理、限流、鉴权)
- "MCP 只在内网给自家 Agent 用" → 需要 Spring Cloud Gateway A2A 专属配置 + 内网鉴权骨架(含 mTLS / 内部 JWT / K8s SA Token 方案对比)
- "已经有一个 API 网关,想复用" → 需要评估现有网关是否支持 SSE、JSON-RPC、防循环,以及改造点