MCP Server 不是“换了 Schema 的 API”。它是一个可能被模型驱动客户端发现和调用的能力边界。生产质量依赖四份契约:
- **Transport:**消息、会话、重连和取消如何传输;
- **Identity:**客户端如何证明 Principal,以及 Claims 如何传播;
- **Capability:**哪些 Tool 存在,这次精确调用是否允许;
- **Data:**结果、分页、来源和大 Artifact 如何暴露。
本文使用 Node.js 风格伪代码说明边界,但不是复制即用的完整 Server。实际实现前,应在仓库中固定 MCP 规范版本和 SDK 版本。
核心结论
- 本地进程使用
stdio,远程部署使用当前 HTTP Transport;旧式 SSE 路由应视为兼容路径,不是通用默认值。 - MCP 只是协议层;网关认证、Resource Authorization、业务策略和审计仍由应用负责。
- Access Token 必须校验可信 Issuer、Audience/Resource、Algorithm、过期时间、Scopes、Key Rotation。
- 不要接受客户端自选 Session ID 作为身份依据;Session 应由 Server 生成并绑定 Principal、Tenant 和 Transport。
- 读写 Tool 分离;资金、删除、发布、对外消息等高影响操作必须进入显式工作流。
- Annotation 是提示,不是保证。
- 大结果优先分页或授权 Resource,不返回无界 JSON。
- Tool 描述、Resource 和 Result 都是不可信输入,可能包含 Prompt Injection。
- 分开限制初始化、发现、调用、重连和结果下载的速率。
- 测试畸形 JSON-RPC、过期 Token、会话混淆、跨租户访问、重连竞态、超大结果和取消。
按部署选择 Transport
stdio
客户端启动本地进程时使用 stdio:
客户端进程
stdin/stdout JSON-RPC
-> MCP Server 进程
-> 本机 OS 权限
如果进程使用专用 OS 用户,并限制文件系统和网络,这可以形成较强边界。但它并不天然安全:继承的环境变量、凭证、Shell 和文件权限仍需审查。
远程 HTTP
远程 Server 还必须处理:
- TLS 和证书校验;
- 身份认证与 Token Audience;
- Origin 与代理行为;
- 会话生命周期和重连;
- 请求大小、速率和并发限制;
- 取消与优雅关闭;
- 租户级鉴权。
当前 MCP 规范定义了 Streamable HTTP。旧式 SSE 加 POST 的 SDK 示例可以用于迁移,但新系统采用前必须核对生命周期和安全行为。
不要把 SSE 描述成“MCP 的流式协议”。SSE 是 HTTP 传输机制,MCP 消息传输和会话语义由选定规范与 SDK 定义。
架构
边界应这样划分:
| 层级 | 负责 | 不能假设 |
|---|---|---|
| Transport | 帧、连接、会话、取消 | 连接已经获得业务授权 |
| Token Validator | 签名、Issuer、Audience、过期、Claims | Claim 自动授予全部对象权限 |
| Session Registry | Server 生成的 Session 与 Principal 绑定 | 客户端提交的 ID 可信 |
| Tool Policy | Capability、Tenant、Object、Purpose、副作用 | 模型选择安全 |
| Tool Handler | 业务校验和事务 | Schema 合法等于拥有对象 |
| Result Layer | 大小、来源、脱敏、分页 | Tool Output 是可信指令 |
OAuth 与 Token 校验
远程 Server 应实现所选 MCP 版本和目标客户端支持的授权流程与 Resource Server 要求。不要在应用代码中发明共享密钥。
至少校验:
- 使用允许 Algorithm 的 Token Signature;
- 可信 Issuer;
- 目标 Resource 或 Audience;
- Expiry 与 Not-before;
- 必需 Scope;
- Tenant 与 Subject Claim;
- Key Rotation 和 Clock Skew Policy。
认证成功后,还要把 Principal 映射到自己的授权服务。Token 声明 scope=orders:read,不代表它一定能读取 Tenant A 的订单 792318。
安全导向的 Middleware 形态
下面是边界片段,不是完整 OAuth 实现:
export async function authenticateRemoteRequest(req, res, next) {
const header = req.headers.authorization ?? "";
const match = /^Bearer ([^\s]+)$/.exec(header);
if (!match) {
return res.status(401).json({ error: "missing_bearer_token" });
}
try {
const claims = await verifyAccessToken(match[1], {
issuer: process.env.OAUTH_ISSUER,
audience: process.env.MCP_RESOURCE,
algorithms: ["RS256", "ES256"], // 仅为示例;应固定 Provider 允许的 Algorithm 集合
});
const subject = typeof claims.sub === "string" ? claims.sub : undefined;
const tenant = typeof claims.tenant_id === "string"
? claims.tenant_id
: undefined;
if (!subject || !tenant) {
throw new Error("required_identity_claim_missing");
}
const rawScope = claims.scope;
const scopeList = Array.isArray(rawScope)
? rawScope.filter((value) => typeof value === "string")
: typeof rawScope === "string"
? rawScope.split(/\s+/).filter(Boolean)
: [];
req.principal = {
subject,
tenant,
scopes: new Set(scopeList),
};
return next();
} catch {
return res.status(401).json({ error: "invalid_access_token" });
}
}
verifyAccessToken 必须使用 Provider 的 JWKS/Key Rotation 实现,拒绝缺失 Issuer 或 Audience,不能回退到开发环境密钥。不要记录 Token,也不要把它写入 Trace Attribute。
会话绑定
Session 是 Server State,不是身份机制。初始化时:
- 认证请求;
- 协商协议版本和 Capabilities;
- 如果 Transport 需要,生成 Server 侧不透明 Session ID;
- 绑定 Subject、Tenant、Scope、Client Identity、过期时间和 Transport;
- 拒绝 Session 绑定与 Token 不一致的请求;
- 在登出、凭证撤销或空闲超时时过期并撤销。
绝不要把客户端提交的 Query Parameter 当作 Session Owner。除非 SDK 明确支持,否则不要把可变 Principal 数据直接挂在 Transport 对象上,应放入类型化 Session Registry。
const sessions = new Map(); // 生产环境应使用有界的持久化/分布式存储
function createSession(principal, transport) {
const id = crypto.randomUUID();
sessions.set(id, {
id,
subject: principal.subject,
tenant: principal.tenant,
scopes: [...principal.scopes],
transport,
// 示例 TTL:应根据部署环境的 Session Policy 推导。
expiresAt: Date.now() + 15 * 60 * 1000,
});
return id;
}
function requireSession(id, principal) {
const session = sessions.get(id);
if (!session || session.expiresAt < Date.now()) {
throw new Error("session_not_found");
}
if (
session.subject !== principal.subject ||
session.tenant !== principal.tenant
) {
throw new Error("session_principal_mismatch");
}
return session;
}
该 Map 只是说明边界。多实例部署还需要有界存储、淘汰、路由或共享状态,以及安全的关闭/重连竞态处理。
Tool 设计与鉴权
Tool 描述和 Input Schema 能帮助模型选择能力,但不能授予权限。
优先设计:
get_order_summary(order_reference)
list_invoice_pages(order_reference, cursor)
request_refund_review(order_reference, reason_code)
避免:
api_request(method, path, headers, body)
每次调用都检查:
- 已认证 Subject 与 Tenant;
- Tool 与 Scope Allowlist;
- 对象所有权和行级策略;
- 参数 Schema 与业务不变量;
- Resource Version 或新鲜度;
- 副作用类型和 Approval;
- 幂等与重试语义;
- 速率、时间和结果预算。
读写 Tool 应分离。让一个 Tool 同时接受 GET 和 DELETE,会把重要策略差异藏在参数里。
Annotation 是提示
readOnlyHint、destructiveHint、idempotentHint、openWorldHint 向 Host 传达预期行为。Server 可能撒谎或出错,Host 也可能采用不同解释。它们适合 UX、审核和保守预检,但真实策略必须由 Server 执行。
有界结果与大数据
大结果会带来内存、延迟、Token 和披露风险。不要假设协议会自动把无界 JSON 流式化。
优先使用:
- **分页:**返回有界 Page、Opaque Cursor 和过期时间;
- **Resource Link:**返回经过独立鉴权的 Resource Reference,而不是完整内容;
- **摘要 + 读取:**先返回统计和相关片段,再读取选定范围;
- **应用下载:**二进制 Artifact 使用短期、范围化的下载流程。
每次分页请求都要重新鉴权。Cursor 不应泄露原始数据库 Offset,也不能让用户切换 Tenant。
function pageResponse(rows, nextCursor) {
const maxBytes = 256 * 1024;
const payload = JSON.stringify({ rows, nextCursor });
if (Buffer.byteLength(payload) > maxBytes) {
throw new Error("result_too_large");
}
return {
content: [{ type: "text", text: payload }],
structuredContent: { count: rows.length, nextCursor },
};
}
二进制数据应使用选定 MCP 版本和客户端支持的 Content Type 与 Resource 语义。Base64 只是编码,不提供保密性,也不能解决尺寸问题;文件较大时优先使用短期授权 Resource。
Result 默认不可信
Tool 和 Resource 内容可能包含:
- Prompt Injection;
- 上游 Bug 导致的过期或跨租户数据;
- HTML、Markdown 链接、代码或文件路径;
- 秘密和个人数据;
- 超大或递归结构。
校验 Result Shape、脱敏不必要字段、附加来源和时间戳、限制深度与字节,并告诉 Host 外部内容是数据而不是指令。Result 不能直接选择高权限 Tool,必须重新通过同一 Policy Engine。
速率、超时与可靠性
分别设置:
- 初始化与 Discovery;
- 活跃 Session 和 Reconnect;
- Subject、Tenant、Server、Tool 级调用;
- 并发和 In-flight Bytes;
- 单 Tool Timeout 和总请求 Deadline;
- Pagination Cursor 生命周期;
- Retry 与 Idempotency;
- Server Shutdown 和 Cancellation。
分类处理故障:
| 故障 | 响应 |
|---|---|
| 畸形请求 | 返回结构化协议错误,不重试 |
| Token 过期 | 重新认证,不盲目重放 |
| 鉴权拒绝 | 停止并审计 |
| 临时读超时 | 仅在安全时有界重试 |
| 写操作歧义超时 | 先按幂等 Key 查询 |
| 结果过大 | 请求更小 Page 或 Resource |
不要把 Stack Trace、Token、SQL 或内部拓扑返回给模型或客户端。
测试与可观测性
在协议和业务边界测试:
- 不支持的初始化版本;
- 畸形 JSON-RPC 和未知 Method;
- 过期、错误 Audience、错误 Tenant、撤销 Token;
- Session Fixation 与 Principal Mismatch;
- Tool Discovery 过滤;
- 跨租户对象访问;
- 错误或缺失 Annotation;
- Pagination Cursor 篡改和过期;
- 超大、畸形、投毒和过期 Result;
- Cancellation、Reconnect、Timeout、Duplicate、Partial Failure。
记录:
- Request 和 Trace ID;
- Protocol 与 Server Version;
- Hash 后的 Subject/Tenant 引用;
- Method、Tool Name、Policy Decision 和 Error Code;
- 字节、行数、延迟、Retry Count 和 Result Classification。
默认不要记录 Bearer Token 或完整敏感参数。使用脱敏、访问控制、采样和保留期限。
生产检查清单
- [ ] 固定并测试 Transport 与 MCP 规范版本。
- [ ] 远程访问使用 TLS 和标准授权流程。
- [ ] 校验 Token Issuer、Audience/Resource、Algorithm、Expiry、Scope、Key Rotation。
- [ ] Session 由 Server 创建、绑定 Principal、有过期和撤销能力。
- [ ] Tool 鉴权检查 Tenant、Object、Purpose 和副作用。
- [ ] 读写能力分离。
- [ ] Annotation 只作为提示,绝不作为权限。
- [ ] Result 有界、经过 Schema 校验、脱敏并带来源。
- [ ] Page 和 Resource Link 每次重新鉴权。
- [ ] 执行 Time、Rate、Concurrency、Retry 和 Byte Budget。
- [ ] Tool Result 不能直接升级为高权限指令。
- [ ] CI 覆盖协议、滥用、重连和跨租户测试。
- [ ] Trace 排除 Token 和不必要个人数据。
常见问题
可以暴露一个通用 HTTP Proxy Tool 吗?
应避免。通用 Proxy 会让鉴权、SSRF、数据外发和审计几乎无法推理。应暴露窄化 Tool,并在代码中校验目标地址。
远程 Transport 会让本地 Server 更不安全吗?
它改变了威胁模型。远程部署增加网络身份、Session、Replay、Rate、Proxy 和多租户风险。本地进程如果继承了宽泛 OS 权限,同样危险。
JWT 是唯一有效的认证方式吗?
不是。应使用部署和 MCP 版本要求的授权机制。JWT Access Token 是常见 Resource Server 表示,但 Token 格式不能替代 Scope 和对象鉴权。
每个 Tool Result 都应该返回 Text 吗?
不应该。客户端可校验的数据使用 Structured Content,大 Artifact 使用 Resource,人类可读摘要才使用 Text。所有结果仍需有界且默认不可信。
总结
企业级 MCP Server 是受策略约束的能力服务。Transport 负责消息抵达;Authentication 识别 Principal;Authorization 决定精确操作是否允许;Result Layer 限制返回内容。
从一个窄化只读 Tool 开始,固定协议和 SDK 版本,先测试负向路径;只有在显式审批和幂等事务之后,再增加写能力。这样得到的是用户可以信任的 Server,而不是贴着 AI 标签的远程 JSON Endpoint。
延伸阅读
- MCP 协议架构与能力边界:协议词汇、生命周期与能力边界。
- 远程 MCP 企业 OAuth 与对象授权:Provider Claim、Tenant Context 与对象鉴权。
- MCP Gateway 会话扩展与背压:会话路由、背压与重连行为。