核心结论
MCP 在多 Agent 系统中的价值,是提供标准集成边界,而不是充当系统宪法。
2026-07-28 规范把协议核心改为无状态:请求自包含,协议级 initialize/initialized 握手被移除,Mcp-Session-Id 也被移除。这能简化路由和水平扩展,但不会让应用自动变成无状态,也不会自动保护委派,更不会消除授权需求。
生产系统应明确分成两个平面:
Agent / Host / MCP Client
|
| MCP 请求与结果
v
MCP Server / 工具适配器
|
| 可信身份 + 策略决策
v
业务服务 / 数据库 / 实际副作用
协议平面:互操作、Schema、请求生命周期
控制平面:身份、授权、审批、限额、状态、审计
即使一个任务经过多个 Agent、服务端与框架,控制平面仍必须保持权威。
MCP 2026-07-28 提供什么
在协议层,MCP 提供:
- JSON-RPC 消息;
- 无状态、自包含请求;
- 工具(Tool)、资源(Resource)与提示词(Prompt);
- 每次请求携带的能力声明与元数据;
- 传输与错误语义;
- Tasks、MCP Apps 等可选扩展;
- 基于成熟 OAuth 与 OpenID Connect 规范的 HTTP 授权机制;
- W3C Trace Context 链路上下文传播约定。
它不能证明:
- 调用者拥有目标对象;
- 一个 Agent 有权向另一个 Agent 委派;
- 请求动作符合用户意图;
- 破坏性副作用已获批准;
- 工具输出可信;
- 重试安全;
- 应用状态仍然有效;
- 工作流应继续执行。
这种分离不是需要绕过的限制,而是正确的所有权边界:协议传递请求,应用执行业务含义。
无状态协议,有状态应用
早期 MCP 版本使用协议级初始化握手与会话标识。在 2026-07-28 中,每个核心请求都带有兼容服务实例处理它所需的信息。
应用仍可有状态,推荐模式是显式、受授权的句柄(Handle):
tools/call browser.create -> { "browser_id": "br_7f..." }
tools/call browser.navigate { "browser_id": "br_7f...", ... }
tools/call browser.close { "browser_id": "br_7f..." }
句柄只是状态引用,本身不授予访问权。
每次使用时,服务端都应验证:
- 已认证主体及其租户是否拥有或允许使用句柄;
- 句柄是否为当前工作负载与用途创建;
- 是否已过期、撤销或进入终态;
- 请求的状态转换是否有效;
- 是否允许并发使用;
- 结果是否有大小限制且隔离正确。
不要只把身份或权限编码在模型可见的不透明字符串中。句柄即使被猜到或泄露,也不能获得访问权。
多 Agent 调用中的身份
一个任务可能同时包含多种身份:
| 身份 | 含义 |
|---|---|
| 资源所有者 | 数据与权限涉及的用户或组织 |
| 已认证主体(Subject) | 访问令牌代表的身份 |
| 客户端(Client) | 向授权服务器注册的 MCP 客户端 |
| 工作负载(Workload) | 执行任务的应用或作业 |
| 执行者(Actor) | 请求当前动作的 Agent 或组件 |
| 委派者(Delegator) | 向另一个执行者分配工作的身份 |
| 审批者(Approver) | 审批有界副作用的人或服务 |
把这些身份压缩成一个 user_id,会破坏问责,并经常意外放大权限。
应由应用或身份系统建立可信执行者链路:
resource_owner -> workload -> planner_agent -> executor_agent -> tool
每一跳只能收窄或保持权限,不能静默扩大。模型生成的“规划者已批准”文本不是委派证明。
HTTP 授权规范覆盖什么
对于受保护的 HTTP 服务,MCP 授权规范基于 OAuth 定义角色:
- MCP Server 作为 OAuth 资源服务器;
- MCP Client 作为 OAuth 客户端;
- 受保护资源元数据标识授权服务器;
resource参数把请求绑定到目标 MCP Server;- 服务端校验访问令牌是否为自身目标受众(Audience)签发;
- 权限范围(Scope)不足时可以触发有界的追加授权流程。
规范明确禁止令牌透传(Token Passthrough):MCP Server 不能接收一个并非为自己签发的令牌,再把它转发给下游 API。
业务授权仍属于应用:
传输层授权:
“这个已认证客户端能否调用受保护的 MCP Server?”
业务授权:
“这个主体能否通过当前工作负载与执行者链路,
为这个用途取消当前状态下的这张特定订单?”
两者缺一不可。
用 Go 演示授权边界
下面的独立 Go 程序演示取消一张订单的策略判断。保存为 main.go 后执行 go run main.go。身份必须来自已验证的认证上下文,订单来自业务数据库,审批来自可信审批存储。调用者只提供订单 ID 和幂等键;不能把模型生成的参数反序列化为这些可信记录。
package main
import (
"errors"
"fmt"
"time"
)
type Principal struct {
SubjectID string
TenantID string
ActorID string
Scopes map[string]bool
}
type CancelRequest struct {
OrderID string
IdempotencyKey string
}
type Order struct {
ID string
TenantID string
OwnerID string
Status string
}
type Binding struct {
TenantID, SubjectID, ActorID string
Operation, OrderID, Key string
}
type Approval struct {
Binding Binding
ExpiresAt time.Time
Revoked bool
Consumed bool
}
func authorizeCancel(now time.Time, p Principal, r CancelRequest, o Order, a Approval) error {
if p.SubjectID == "" || p.TenantID == "" || p.ActorID == "" {
return errors.New("identity_required")
}
if r.OrderID == "" || r.IdempotencyKey == "" {
return errors.New("request_incomplete")
}
if !p.Scopes["orders:cancel"] {
return errors.New("insufficient_scope")
}
if r.OrderID != o.ID || p.TenantID != o.TenantID || p.SubjectID != o.OwnerID {
return errors.New("object_not_authorized")
}
if o.Status != "open" {
return errors.New("invalid_state")
}
expected := Binding{
TenantID: p.TenantID, SubjectID: p.SubjectID, ActorID: p.ActorID,
Operation: "orders.cancel@1", OrderID: r.OrderID, Key: r.IdempotencyKey,
}
if a.Binding != expected || a.Revoked || a.Consumed || !now.Before(a.ExpiresAt) {
return errors.New("approval_invalid")
}
return nil
}
func main() {
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
p := Principal{"user-1", "tenant-1", "executor-1", map[string]bool{"orders:cancel": true}}
r := CancelRequest{"order-1", "cancel-1"}
o := Order{"order-1", "tenant-1", "user-1", "open"}
a := Approval{
Binding: Binding{
TenantID: p.TenantID, SubjectID: p.SubjectID, ActorID: p.ActorID,
Operation: "orders.cancel@1", OrderID: r.OrderID, Key: r.IdempotencyKey,
},
ExpiresAt: now.Add(time.Minute),
}
fmt.Println(authorizeCancel(now, p, r, o, a))
r.OrderID, o.ID = "order-2", "order-2"
fmt.Println(authorizeCancel(now, p, r, o, a))
}
预期输出依次是 <nil> 和 approval_invalid:即使两张订单属于同一用户,订单 1 的审批也不能授权取消订单 2。函数只判断可信快照,不负责验证令牌,也没有执行或持久化取消操作。
业务服务必须在执行时重新检查身份、当前策略、订单状态及审批有效性,并原子完成审批消费、订单修改和幂等结果持久化。同一键与同一请求指纹的重试应返回已记录结果,指纹冲突则拒绝。单靠这个内存函数无法阻止并发重放。
审批必须绑定具体操作
审批至少应绑定:
- 已认证主体;
- 受委派执行者或工作负载;
- 工具与操作版本;
- 目标对象或有界集合;
- 关键参数;
- 允许的副作用;
- 有效期;
- 最大使用次数;
- 关联 ID 与幂等键。
“批准所有未来动作”不是安全捷径。批准一份计划,也不代表之后模型生成的每个参数都自动获得覆盖。
MCP 协议可以承载输入请求或任务状态,但应用必须定义审批语义、安全持久化,并在副作用发生前立即校验。
结构校验不能代替授权
工具的 Schema 很有价值:
- 客户端可以发现必填字段;
- 错误结构能被提前拒绝;
- 枚举与上下界可以缩小输入范围;
- 输出可以校验;
- 契约可以版本化。
但 Schema 无法确认:
- 调用者是谁;
- 记录是否属于调用者的租户;
- 当前值是否仍有效;
- 价格、余额或额度是否变化;
- 动作是否已批准;
- 重复请求是否已经成功。
除非来自可信、固定来源,否则工具描述、注解、资源内容与远程 Schema 都应视为不可信。
工具结果也是输入边界
第二个 Agent 可能消费第一个 Agent 的工具结果,但这不会让结果自动成为指令。
工具与资源结果可能包含:
- 提示注入;
- 攻击者控制的 URL;
- 超大或递归数据;
- 带主动内容的 HTML 或文件;
- 过期或相互冲突的记录;
- 凭证与个人数据;
- 冒充策略或审批的文本。
应执行:
- MIME 与文件类型白名单;
- 字节、行数、嵌套深度与重定向次数限制;
- URL 与网络出口策略;
- 提示词与界面中的内容隔离;
- 适用场景下的恶意文件与压缩包扫描;
- 来源与新鲜度元数据;
- 留存与删除规则。
Host 应把外部内容展示为证据,不能把它合并进可信系统指令。
混淆代理与委派
多 Agent 系统会放大混淆代理(Confused Deputy)问题:高权限执行者可能从低信任规划者收到看似合理的请求,并用更广凭据执行。
防御措施包括:
- 保留原始主体与执行者链路;
- 为精确服务签发绑定目标受众的短期凭证;
- 把权限范围限制在委派任务内;
- 由业务服务执行对象级检查;
- 禁止 Agent 自行选择身份或目标受众;
- 把审批绑定到参数和副作用;
- 拒绝令牌透传;
- 审计委派与策略决策。
如果使用令牌交换,它必须是带显式策略的身份系统操作,不能把 Bearer Token 直接复制进另一个 Agent 的上下文。
副作用、重试与未知结果
工具调用的失败状态不止“成功”和“错误”:
not_started
started
committed
response_lost
outcome_unknown
compensated
对于有副作用的工具:
- 要求持久化幂等键;
- 保存请求指纹与终态结果;
- 区分安全重试与结果对账;
- 支持按操作 ID 查询状态;
- 尽可能使用事务或事务发件箱(Outbox);
- 把补偿与重试分开定义;
- 不允许模型根据超时推断成功与否。
如果下游已经提交、但执行者在收到响应前超时,必须查询结果。盲目重试可能重复付款、消息、工单或部署。
并发与共享状态
多个 Agent 可能同时修改同一对象。协议消息顺序不是业务并发控制。
应使用负责该对象的系统所提供的机制:
- 乐观版本号或 ETag;
- 数据库事务;
- 比较并交换(CAS);
- 带清晰有效期的租约或锁;
- 工作流状态机;
- 单写者队列;
- 包含当前状态的冲突响应。
应用应决定 Agent 是否允许并行,以及结果如何合并。“协调者会想办法”不是一致性模型。
控制平面检查表
| 控制 | 执行点 |
|---|---|
| 令牌签发者、目标受众、有效期 | MCP Server / 资源服务器 |
| 主体、租户、受委派执行者 | 可信身份中间件 |
| 对象与字段访问 | 业务服务 |
| 工具白名单与参数限制 | Host 与工具适配器 |
| 审批绑定 | 应用策略服务 |
| 状态转换 | 负责状态的工作流或数据库 |
| 幂等与结果对账 | 负责副作用的服务 |
| 时间、轮次与成本预算 | Agent 运行时 / Host |
| 文件系统与网络隔离 | 沙箱基础设施 |
| 结果限制与内容信任 | 服务端、Host 与渲染器 |
| 审计与链路关联 | 每个边界 |
重复关键检查可以形成纵深防御,但不要模糊权威归属。拥有数据与副作用的服务始终是最终执行点。
失败测试
至少测试:
| 场景 | 期望行为 |
|---|---|
| 令牌属于另一目标受众 | 工具执行前拒绝 |
| 跨租户对象 ID | 在业务服务拒绝 |
| 执行者收到规划者的令牌 | 拒绝透传或错误目标受众 |
| 审批已过期或被篡改 | 拒绝副作用 |
| 响应丢失后的重复调用 | 返回已记录结果,不重复执行 |
| 状态句柄泄露 | 主体与租户不匹配时拒绝 |
| 工具结果中的注入指令 | 作为数据,不作为策略 |
| 两个 Agent 更新同一版本 | 一个成功,另一个得到明确冲突 |
| 任务中途进程重启 | 从持久化应用状态恢复 |
| 取消与提交竞争 | 返回已定义的对账结果 |
这些测试必须运行在组合系统上,不能只测试模拟的策略函数。
不依赖隐藏推理的可观测性
使用协议定义的 Trace Context 关联:
用户请求 -> Host -> MCP Client -> MCP Server -> 业务服务
记录:
- 链路与关联 ID;
- 已认证主体、租户、工作负载、执行者与委派者 ID;
- 工具名、契约版本与服务版本;
- 脱敏后的参数指纹;
- 策略决策与原因码;
- 审批引用;
- 操作 ID 与幂等键;
- 状态转换;
- 结果与失败类别;
- 延迟与有界用量。
不要把隐藏思维链留存设为治理要求。它既不是可靠策略记录,也不是隐私安全默认项。可观察动作与显式决策才是审计来源。
常见问题
MCP 是授权或策略引擎吗?
不是。HTTP 授权规范提供 OAuth 边界,但数据所属服务仍须判断:这个主体及执行者,是否可以对当前状态下的这个对象执行这项操作。
MCP 2026-07-28 如何处理状态?
协议核心移除了初始化握手与会话 ID。应用可以用显式句柄保留状态,但每次查找都需要授权、过期检查和合法的状态转换。
能直接把 MCP 访问令牌交给另一个 Agent 吗?
不要用复制 Bearer Token 的方式实现委派。应通过身份系统批准的流程约束执行者、目标受众、权限范围和有效期。MCP 禁止接收面向其他资源的令牌并透传给下游 API。
JSON Schema 能保证工具调用安全吗?
它只能校验已表达的输入约束,不能证明所有权或审批。格式正确的 order_id 仍可能属于另一个租户,执行前必须查询可信记录并授权。
多 Agent 审计应记录什么?
记录已验证身份、委派、策略与审批决策、工具版本、脱敏参数指纹、状态转换和最终结果,并用关联 ID 串联。不需要保存隐藏推理或原始凭证。
总结
MCP 为多 Agent 系统提供标准协议边界。2026-07-28 无状态核心让边界更容易路由、缓存、追踪与扩展;应用需要状态时,可以使用显式句柄。
安全性来自另一个被强制执行的控制平面:绑定目标受众的身份、显式委派、对象级授权、有界审批、事务内状态检查、幂等、并发控制、不可信结果处理,以及可重建的审计记录。
最持久的规则很简单:协议元数据可以描述请求,只有可信应用策略才能授权请求的业务含义与副作用。
一手来源
- MCP 2026-07-28 Specification
- MCP 2026-07-28 Authorization
- MCP Security Best Practices
- MCP 2026-07-28 Release Candidate 说明
- OAuth 2.0 Resource Indicators, RFC 8707
- OAuth 2.0 Protected Resource Metadata, RFC 9728