Model Context Protocol(MCP)正在成为 AI 应用连接外部工具的事实标准。但当你把一个本地跑通的 MCP Server 部署到生产环境时,会发现官方文档几乎没提网关层该怎么设计——认证怎么做?多实例怎么路由?SSE 连接断了怎么恢复?监控指标埋哪些?这些问题才是决定你的 MCP 服务能不能扛住真实流量的关键。
根据 Anthropic 公开的数据,2026 年 Q1 MCP 生态的工具服务器数量已突破 12,000 个,但其中能称为「生产级」的不到 5%。绝大多数开发者卡在了「本地 demo 能跑」和「线上稳定服务」之间的鸿沟上。
本文将从传输层协议选型出发,逐层拆解 MCP 网关的核心架构,给出完整的代码实现和避坑指南。
🔌 一、MCP 传输层深度解析
MCP 协议基于 JSON-RPC 2.0 构建,官方支持两种传输方式:stdio 和 HTTP+SSE。选错传输方式是生产部署的第一个坑。
1.1 stdio vs HTTP+SSE:选型决策树
stdio 模式下,客户端通过子进程的标准输入/输出与 MCP Server 通信,延迟极低(进程内管道),但无法跨网络,也无法水平扩展。HTTP+SSE 模式则支持远程访问、负载均衡和多客户端并发。
| 维度 | stdio | HTTP+SSE |
|---|---|---|
| 网络延迟 | < 1ms(进程管道) | 10-100ms(取决于网络) |
| 并发能力 | 单客户端独占 | 多客户端并发 |
| 水平扩展 | ❌ 不支持 | ✅ 负载均衡 |
| 认证机制 | OS 级进程隔离 | Token / mTLS |
| 适用场景 | 本地 IDE 插件 | 云服务 / 团队共享 |
⚡ **关键结论:**如果你的 MCP Server 只给本地 IDE 用,选 stdio;只要涉及远程访问、多用户或需要可观测性,就必须上 HTTP+SSE。
1.2 SSE 连接的生命周期管理
HTTP+SSE 模式的核心是用 SSE(Server-Sent Events)做服务端到客户端的单向推送,用普通 HTTP POST 做客户端到服务端的请求。这个设计看起来简单,但生产环境中有三个致命问题:
- 连接泄漏:客户端异常断开后,服务端不知道,SSE 连接一直占着资源
- 消息乱序:网络抖动导致 SSE 消息到达顺序与发送顺序不一致
- 超时断连:Nginx/Cloudflare 等中间层会在空闲 60 秒后主动断开 SSE 连接
下面是一个生产级的 SSE 连接管理器实现:
// SSE 连接管理器:处理心跳、重连与消息队列
interface SSEConnection {
id: string
controller: ReadableStreamDefaultController
lastHeartbeat: number
messageQueue: QueuedMessage[]
clientCapabilities: MCPCapabilities
}
class MCPSSEManager {
private connections = new Map<string, SSEConnection>()
private heartbeatInterval = 30_000
constructor() {
setInterval(() => this.cleanup(), 10_000)
setInterval(() => this.heartbeat(), this.heartbeatInterval)
}
createConnection(request: Request): Response {
const connectionId = crypto.randomUUID()
const stream = new ReadableStream({
start: (controller) => {
const conn: SSEConnection = {
id: connectionId, controller,
lastHeartbeat: Date.now(),
messageQueue: [], clientCapabilities: {},
}
this.connections.set(connectionId, conn)
this.send(conn, 'connection', { id: connectionId })
},
cancel: () => { this.connections.delete(connectionId) },
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'X-Connection-Id': connectionId,
},
})
}
send(conn: SSEConnection, event: string, data: unknown): void {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
try {
conn.controller.enqueue(new TextEncoder().encode(message))
conn.lastHeartbeat = Date.now()
} catch { this.connections.delete(conn.id) }
}
private heartbeat(): void {
for (const conn of this.connections.values())
this.send(conn, 'heartbeat', { ts: Date.now() })
}
private cleanup(): void {
const now = Date.now()
for (const [id, conn] of this.connections) {
if (now - conn.lastHeartbeat > 300_000) {
try { conn.controller.close() } catch {}
this.connections.delete(id)
}
}
}
}
💡 **提示:**SSE 连接的
event:字段就是你区分消息类型的利器。MCP 的 JSON-RPC 响应用event: message,心跳用event: heartbeat,这样客户端可以按事件类型分发处理。
1.3 JSON-RPC 2.0 消息路由
MCP 的所有请求/响应都走 JSON-RPC 2.0 格式。网关层需要解析 method 字段做路由分发,核心方法包括 initialize、tools/list、tools/call、resources/read 等。
// JSON-RPC 路由器:根据 method 分发到不同 handler
interface JSONRPCRequest {
jsonrpc: '2.0'; id: string | number; method: string; params?: Record<string, unknown>
}
interface JSONRPCResponse {
jsonrpc: '2.0'; id: string | number; result?: unknown
error?: { code: number; message: string; data?: unknown }
}
type MethodHandler = (params: Record<string, unknown>, ctx: RequestContext) => Promise<unknown>
type Middleware = (req: JSONRPCRequest, ctx: RequestContext) => Promise<JSONRPCResponse | null>
class MCPRouter {
private handlers = new Map<string, MethodHandler>()
private middlewares: Middleware[] = []
method(name: string, handler: MethodHandler): void { this.handlers.set(name, handler) }
use(middleware: Middleware): void { this.middlewares.push(middleware) }
async route(request: JSONRPCRequest, context: RequestContext): Promise<JSONRPCResponse> {
for (const mw of this.middlewares) {
const rejected = await mw(request, context)
if (rejected) return rejected
}
const handler = this.handlers.get(request.method)
if (!handler) {
return { jsonrpc: '2.0', id: request.id,
error: { code: -32601, message: `Method not found: ${request.method}` } }
}
try {
const result = await handler(request.params ?? {}, context)
return { jsonrpc: '2.0', id: request.id, result }
} catch (err) {
return { jsonrpc: '2.0', id: request.id,
error: { code: -32603, message: err instanceof Error ? err.message : 'Internal error' } }
}
}
}
type RequestContext = {
connectionId: string; tenantId?: string; userId?: string; permissions: string[]
}
这个路由器的设计有几个关键点:中间件链是同步串行执行的(for...of 而非 Promise.all),确保认证在限流之前、限流在日志之前;JSON-RPC 错误码遵循标准定义(-32601 是 method not found,-32603 是 internal error)。
🔐 二、认证、多租户与安全加固
MCP 协议本身没有定义认证机制——这是有意为之的设计决策,让传输层自行处理。但在生产环境中,这意味着你必须自己搭建完整的认证体系。
2.1 Bearer Token 认证中间件
最常用的方案是 Bearer Token,适合 API 调用场景。对于团队内部使用的 MCP 服务,JWT(JSON Web Token)是最实用的选择:
// Bearer Token 认证中间件
import { jwtVerify } from 'jose'
function createAuthMiddleware(secret: Uint8Array): Middleware {
return async (request, context) => {
if (request.method === 'initialize') return null // 握手阶段不需认证
if (!context.userId) {
return { jsonrpc: '2.0', id: request.id,
error: { code: -32000, message: 'Unauthorized: missing or invalid token' } }
}
if (request.method === 'tools/call' && !context.permissions.includes('write')) {
return { jsonrpc: '2.0', id: request.id,
error: { code: -32001, message: 'Forbidden: insufficient permissions' } }
}
return null
}
}
// SSE 建立连接时验证 token
async function authenticateConnection(request: Request, secret: Uint8Array): Promise<RequestContext> {
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) return { connectionId: '', permissions: [] }
try {
const { payload } = await jwtVerify(authHeader.slice(7), secret, { algorithms: ['HS256'] })
return {
connectionId: crypto.randomUUID(),
userId: payload.sub as string,
tenantId: payload.tenant as string,
permissions: (payload.perms as string[]) ?? ['read'],
}
} catch { return { connectionId: '', permissions: [] } }
}
⚠️ **警告:**永远不要在 JSON-RPC 的
params里传递认证信息。token 应该只在 HTTP 头(Authorization)中传输,params 里传的是业务数据。
2.2 多租户隔离策略
当你的 MCP 服务为多个团队/客户提供工具时,租户隔离是必须的。核心原则是:每个 SSE 连接绑定一个租户,所有请求自动继承租户上下文。
| 隔离层级 | 实现方式 | 安全性 | 性能影响 |
|---|---|---|---|
| 连接级 | SSE 连接绑定 tenantId | ⭐⭐⭐⭐⭐ | 无 |
| 请求级 | 每个请求携带 tenantId | ⭐⭐⭐ | 轻微 |
| 数据级 | 数据库行级安全策略 | ⭐⭐⭐⭐ | 中等 |
| 进程级 | 每租户独立进程 | ⭐⭐⭐⭐⭐ | 高(资源浪费) |
推荐的做法是 连接级 + 数据级 的组合:SSE 建立时确定租户,所有数据库查询自动注入租户过滤条件。这样即使开发者忘了手动加 WHERE tenant_id = ?,也不会泄露数据。
2.3 输入验证与 Prompt Injection 防御
MCP 工具的 tools/call 接口是攻击者最眼馋的入口——通过精心构造的参数,可能诱导 AI 执行非预期操作。
防御策略包括:
- ✅ 参数白名单校验:每个工具定义严格的 JSON Schema,拒绝未知字段
- ✅ 输出长度限制:工具返回值设上限,防止通过超大响应消耗 Token
- ✅ 调用频率限制:单用户每分钟最多 N 次 tools/call
- ❌ 不要信任 AI 的判断:AI 可能被 Prompt Injection 诱导调用不该调用的工具
📊 三、可观测性与生产运维
一个没有监控的 MCP 网关就是在裸奔。你需要知道:谁在调用?调了什么工具?耗时多久?失败率多少?
3.1 核心监控指标
以下是最小可行的监控指标集:
// MCP 网关核心监控指标
interface MCPMetrics {
activeConnections: number
connectionRate: number
requestCount: Map<string, number> // 按 method 统计
requestLatency: Map<string, number[]> // 按 method 的延迟分布
errorRate: Map<string, number>
toolCallCount: Map<string, number> // 按工具名统计
toolCallLatency: Map<string, number[]>
}
// 用中间件自动采集请求指标
function metricsMiddleware(): Middleware {
return async (request, context) => {
incrementCounter('mcp_requests_total', {
method: request.method, tenant: context.tenantId
})
return null
}
}
📌 **记住:**Prometheus 指标名用
mcp_前缀,避免与其他服务的指标名冲突。推荐使用 prom-client 库来注册和暴露指标。
3.2 分布式追踪
当 tools/call 触发多个下游调用时,用 OpenTelemetry 追踪请求链路是定位瓶颈的最佳方式。核心思路是用 tracer.startActiveSpan 包裹工具调用,自动记录工具名、租户、用户 ID 等属性,失败时标记错误状态并记录异常。建议使用 @opentelemetry/sdk-node 一键集成,配合 Jaeger 或 Tempo 做可视化。
3.3 告警规则设计
以下是生产环境推荐的告警阈值:
| 指标 | 告警条件 | 严重程度 | 处理方式 |
|---|---|---|---|
| SSE 连接数 | > 1000(单实例) | ⚠️ 警告 | 扩容 |
| tools/call 错误率 | > 5%(5 分钟窗口) | 🔴 严重 | 检查工具实现 |
| tools/call P99 延迟 | > 10 秒 | ⚠️ 警告 | 优化慢查询 |
| 认证失败率 | > 20% | 🔴 严重 | 可能遭受攻击 |
| 心跳超时 | > 3 次连续 | ⚠️ 警告 | 检查网络 |
🛡️ 四、避坑指南与生产 Checklist
基于大量 MCP 生产部署的经验教训,这里列出最常见的坑:
传输层坑:
- ❌ SSE 空闲超时:Nginx 默认
proxy_read_timeout 60s,SSE 连接 60 秒没数据就断了。改为proxy_read_timeout 86400s并加心跳 - ❌ SSE 重连风暴:客户端断连后同时重连,瞬间打满服务端连接数。需要客户端加指数退避(exponential backoff)
安全坑:
- ❌ 信任客户端传来的 connectionId:攻击者可以伪造 connectionId 接管别人的连接。connectionId 必须由服务端生成
- ❌ 没有限流:一个恶意客户端可以无限调用
tools/call,耗尽 Token 预算或下游资源 - ❌ 日志泄露敏感数据:
tools/call的参数可能包含 API Key 等,日志必须脱敏
运维坑:
- ❌ 单进程部署:Node.js 单进程挂了整个服务就没了。用 PM2 cluster 模式或 Kubernetes 多副本
- ❌ SSE 连接无法优雅关闭:部署新版本时先发
event: shutdown再关闭,不要直接杀进程
✅ **推荐做法:**部署前用以下 Checklist 逐项检查,全部通过才能上线。
生产部署 Checklist:
- [ ] SSE 心跳间隔 ≤ 30 秒
- [ ] Nginx
proxy_read_timeout≥ 86400 秒 - [ ] Bearer Token 认证已启用
- [ ] tools/call 参数 Schema 校验已启用
- [ ] 单用户速率限制已配置(建议 60 次/分钟)
- [ ] Prometheus 指标已暴露
- [ ] 健康检查端点
/health已实现 - [ ] 日志脱敏规则已配置
- [ ] 优雅关闭流程已测试
- [ ] 客户端指数退避重连已验证
🎯 总结
MCP 网关的核心挑战不在协议本身,而在「连接管理」「认证隔离」「可观测性」这三个生产级需求上。本文给出的架构方案适用于大多数场景:
- 传输层:HTTP+SSE 是远程部署的唯一选择,心跳和重连机制是必须的
- 认证层:Bearer Token + JWT 是最实用的方案,连接级租户隔离足够安全
- 监控层:Prometheus 指标 + OpenTelemetry 追踪 + 告警规则,三件套缺一不可
- 安全层:输入白名单校验、速率限制、日志脱敏,三道防线
如果你正在搭建 MCP 服务,建议先跑通 stdio 模式验证工具逻辑,再用本文的网关架构做远程化改造。不要跳过 stdio 直接上 SSE——你会分不清是工具逻辑的 bug 还是传输层的 bug。
相关工具推荐:
- 🔧 MCProxy — 官方 MCP 代理,支持 stdio → HTTP+SSE 转换
- 🔧 SSEKit — 生产级 SSE 服务端框架
- 🔧 Zod — MCP 工具参数 Schema 校验的首选库
- 🔧 OpenTelemetry Node SDK — 分布式追踪一键集成
- 📝 jsjson.com 在线 JSON 校验工具 — 快速验证你的 JSON-RPC 消息格式是否正确