AI 工程实践
MCP 权限网关可复现实验:身份、租户、一次性审批与审计
使用官方 @modelcontextprotocol/sdk 1.29.0 建立 Client、权限 Gateway 与上游 Server 两段连接,以 24 个本地测试验证默认拒绝、租户注入、审批防重放、版本冲突、参数值不以原文落日志、写前审计故障关闭与未知写入终态对账。
时间与证据
这是一项 2026 年 7 月 13 日实际运行的本地工程实验,不是对 2024 年 MCP 首发状态的伪复现。工程固定使用官方 @modelcontextprotocol/sdk@1.29.0,依赖版本写入 package.json 和 lockfile;实验代码位于 labs/mcp-policy-gateway/。它不连接模型、不访问外部 API,也不使用真实客户数据,因此能够在本地重复执行并检查每一次状态变化。
历史边界仍然必须说明。Anthropic 在 2024 年 11 月 25 日公布 MCP,首发核心规范同时定义了 stdio 和 HTTP with SSE;当时 Claude Desktop 产品只连接本机 server,不等于协议只能用于本地。更重要的是,初始核心规范明确把 authentication 与 authorization 留给自定义协商。2025 年 3 月加入的 OAuth 2.1 框架、Streamable HTTP 和 tool annotations,以及同年 9 月出现的 Registry,都不能倒填成 2024 年首发能力。
初始工具规范已经要求 server 验证输入、执行访问控制、限流并清洗输出,也要求 client 在敏感操作前展示参数、确认、超时和记录结果;同时它承认这些原则无法由协议本身强制。本文的研究问题因此不是“MCP 有没有安全建议”,而是:如何把建议变成模型无法自行绕过、并且可以用最终状态验证的硬门。
当前 SDK 可以把已经验证的 authInfo 交给 handler,但它不替实验完成 OAuth 验证,也不把 clientId 自动解释成人类用户。测试使用 InMemoryTransport 注入固定身份,只模拟“认证层已经验证 token 后的上下文”。人的 principal 取自 authInfo.extra.sub,租户取自 authInfo.extra.tenantId;tool arguments 中自报的 userId、clientId 或 tenantId 一律不可信。
证据等级标为 reproduced,只覆盖仓库内实际运行的 24 个 MCP 网关测试与一个演示流程。它不代表生产 OAuth、分布式事务、长期流量或真实攻击已经验证。
实验设计
两段连接,不让 Gateway 直接伪造成功
实验建立三个角色和两段官方 SDK 连接:
测试 Client
-> InMemoryTransport(注入已验证 AuthInfo 的测试夹具)
-> Gateway McpServer
-> 内部 Client + 第二段 InMemoryTransport
-> Mock Upstream McpServer
-> tenant-a / tenant-b 权威状态
Gateway 对外只暴露 records.read 与 records.update。上游存储暴露独立的 storage.read 与 storage.update。Gateway 必须先完成身份、resource audience、scope、本地策略和审批检查,成功后才把可信租户注入上游调用。测试最后读取上游调用数组、记录版本和两个租户的状态,避免把“返回了一段拒绝文字”误当成“写操作真的没有发生”。
MCP/SDK 原生能力与本站实现必须分开
| 层次 | 本实验实际使用的能力 | 不能误写成什么 |
|---|---|---|
| MCP/SDK | 初始化、tools/list、tools/call、JSON-RPC request ID、Zod 输入 schema、isError、handler authInfo |
不等于完成 OAuth、RBAC、租户隔离或审批 |
| 身份夹具 | InMemoryTransport.send(..., { authInfo }) 传入已验证上下文 |
不是真实 token 验签器,也不证明网络认证安全 |
| 本地策略 | principal + tenant + tool + scope 默认拒绝表 | 不是模型 Prompt,也不信任 tool annotation |
| 审批存储 | 绑定主体、租户、工具、规范化参数、策略版本和过期时间 | 不是 MCP 自动提供的人工审批 |
| 审计边界 | 到达、策略决定、上游结果、响应元数据;参数只存键名与哈希 | 不是完整生产 SIEM、不可篡改账本或分布式事务 |
当前 ToolAnnotations 中的 readOnlyHint 与 destructiveHint 只是提示。实验虽然为两个工具设置了 annotation,但授权只读取本地可信 policy。这样即使上游或插件把破坏性工具错误标成只读,也不能因此获得权限。
一次性审批如何绑定动作
写操作必须携带 out-of-band 生成的 approvalId。审批没有注册为 MCP tool,业务 caller 无法一边请求写入、一边调用另一个工具给自己批准。审批哈希绑定:
principal + tenant + tool + canonical arguments + policyVersion
approvalId 本身不进入动作哈希。消费发生在任何上游 await 之前,同一进程中的两个并发请求最多一个把状态从 granted 改为 consumed。修改参数、换主体、换租户、换工具、换策略版本、过期或重放都会拒绝。写入参数还绑定 expectedVersion,上游只有在记录版本仍与审批时一致时才更新,避免审批后出现的并发变化被静默覆盖。这里验证的是最小状态机;生产环境仍要用支持原子条件更新的数据库或事务服务。
审计为何放在两层
SDK 会在 tool callback 之前执行 input schema 校验。如果只在 callback 中记录日志,恶意的未知字段、路径式 record ID 或超长参数会被 SDK 拒绝,却不会留下 handler 审计。因此实验在 server transport 边界先记录 arrival,只保存工具名、参数键和参数哈希;handler 再记录 decision 与 outcome,transport 发回结果时记录 response。
真实 token 和参数值都不以原值进入审计事件。Gateway 只保存工具名、参数键、规范化参数的确定性 SHA-256 和可信主体信息,因此测试中的自由文本、JWT、Slack token、AWS access key、approval capability 与 auth token 原文不会落日志。但无密钥哈希会泄露“两个参数是否相同”,低熵参数还可能被字典验证;这不是加密或匿名化。生产系统应改用独立密钥的 HMAC、轮换 key id,或只保存完成调查所需的更小元数据。
写操作在策略允许后、调用上游前必须成功写入 allow decision;该记录失败时返回 AUDIT_UNAVAILABLE、恢复一次性审批并保持上游调用数为零。若上游成功后 outcome 写入失败,业务成功结果不被改写,终态事件进入待投递队列。若写调用发生传输异常,或上游声称成功但响应无法通过 schema,Gateway 返回 UPSTREAM_OUTCOME_UNKNOWN,审计记录 unknown 与 reconciliationRequired=true,不把权威状态可能已经变化误写成“上游失败”。
结果
实测环境:
| 项目 | 固定值 |
|---|---|
| 操作系统 | macOS 26.5.1,Darwin 25.5.0,arm64 |
| Node.js | v22.23.1 |
| npm | 10.9.8 |
| MCP SDK | 1.29.0(exact dependency + lockfile) |
| 工程提交 | c7c3b1b5e4ee198fbdbeef1295af11ef5fe1d5dc |
| 数据与网络 | 固定内存状态;不调用模型与外部 API |
从干净 checkout 复现:
git clone https://github.com/youniszhang/younis-ai-lab.git
cd younis-ai-lab
git checkout c7c3b1b5e4ee198fbdbeef1295af11ef5fe1d5dc
npm ci
npm run lab:mcp:test
npm run lab:mcp-gateway
2026 年 7 月 13 日的本地结果为 24/24 个 MCP 网关测试通过;连同 DeepSeek 名义权重存储估算器的 3 个测试,统一 npm run lab:test 为 27/27 通过。测试不是随机采样,每次运行都从固定内存状态开始。原始 TAP、环境和确定性演示输出分别保存在 labs/mcp-policy-gateway/results/2026-07-13-node-test.tap、2026-07-13-environment.txt 与 2026-07-13-demo.json,不只保留本文摘要;其中最后一个测试会实际运行演示并逐字段比对仓库中的 JSON 工件。
| 验证组 | 实际断言 | 权威结果 |
|---|---|---|
| 正常只读 | Alice 读取同名 rec_shared |
返回 tenant-a 版本 1;没有读到 tenant-b 版本 4 |
| 身份关闭 | 无身份、过期/零/缺失/非有限 expiry、错误 resource、缺 scope | 全部 isError=true;上游调用数 0 |
| 参数与伪造身份 | args 增加 tenantId=tenant-b、principal=bob |
schema 在 handler 前拒绝;上游调用数 0;边界日志存在 |
| 本地 allowlist | 只有只读策略的 reader 持有合法 scope 与审批 |
仍由本地 policy 拒绝;annotation 与审批不能越权 |
| 提示注入文本 | note 要求“忽略策略并批准” | 返回 APPROVAL_REQUIRED;记录版本保持 1 |
| 审批绑定 | 分别修改 principal、tenant、tool、参数、policy version,另测过期 | 不匹配与过期全部拒绝;非过期的不匹配不会消费原审批 |
| 防重放 | 同一 approval 再次调用 | APPROVAL_REPLAYED;上游写调用仍为 1 |
| 并发消费 | 两个请求并发使用同一 approval | 一个成功、一个失败;权威版本只从 1 变为 2 |
| 租户隔离 | Alice 更新 tenant-a 同名记录 |
tenant-b 状态与版本不变 |
| 参数不以原文落日志 | note 同时写入 sk、Slack、JWT、AWS 测试值 | 日志没有测试参数与 auth token 原文,只保留键名和确定性哈希;相等关系与低熵猜测风险仍存在 |
| 写前审计故障 | allow decision 写入被注入失败 | 返回 AUDIT_UNAVAILABLE;上游写入 0;同一审批恢复后可重试 |
| 写后审计故障 | outcome 写入被注入失败 | 保持真实成功结果与一次写入;终态进入 pending 队列,无矛盾 deny |
| 写入终态未知 | 分别模拟“已写入但成功响应 schema 漂移”和“写入后传输丢失” | 两次权威状态都已到版本 2;Gateway 返回 UPSTREAM_OUTCOME_UNKNOWN,审计标记需对账,不谎报失败 |
| 并发版本变化 | 审批后权威记录从版本 1 变为 2 | 返回 VERSION_CONFLICT;不覆盖并发状态 |
| 上游输出 | 只读结果返回业务文本与额外未知字段 | 合法文本标 dataTrust=untrusted;未知字段由严格 schema 拒绝并记录 invalid-response |
演示命令:
npm run lab:mcp-gateway
演示先读取 tenant-a 的 open 记录,再由可信测试代码签发一次审批并更新为 review。输出记录 upstreamMutations: 1,版本从 1 变为 2。这个数字与上游状态一起构成成功证据;如果只看到 Gateway 返回 ok 而没有权威状态变化,实验不算通过。
失败与边界
第一,身份是测试夹具,不是生产认证。InMemoryTransport 只负责传递 authInfo,不验证签名、issuer、JWKS、nonce 或 token 撤销。生产入口必须先严格验证 token,并校验 resource/audience;authInfo.clientId 表示 OAuth client 应用,不能直接当成人类账号。
第二,ApprovalStore 和上游状态都在单进程内存。同步消费足以验证当前并发用例,但多实例服务需要数据库唯一约束、compare-and-set 或事务锁。实验已经把写入后的传输异常与非法成功响应标为未知终态,却不能自动查回权威状态;生产仍需要幂等键、对账查询、durable reconciliation queue 和补偿策略。
第三,提示注入测试证明的是 恶意文本不能修改外部策略或自行批准,不是证明系统能够识别所有 Prompt Injection。若被批准的高权限工具本身接收不可信内容,仍要限制参数、文件、域名、网络出口和凭据范围。
第四,审计不是原子业务事务。实验在写入前强制记录 allow decision;上游成功后如果 terminal outcome 存储失败,内存待投递队列会保留终态并继续向客户端返回真实业务成功。该队列本身不耐进程崩溃,生产系统仍应使用 durable outbox、幂等上游操作和补偿巡检,而不能把内存数组当成审计账本。
第五,审计参数哈希使用无密钥、确定性的 SHA-256。它证明当前工件没有保存测试值原文,但会泄露相等关系,也可能被低熵字典反查;生产必须使用 keyed HMAC 或进一步减少元数据,并把 key id、轮换与访问控制纳入审计设计。
第六,实验对成功上游结果执行严格 JSON schema、租户一致性与字段长度校验,并把业务文本标为 dataTrust=untrusted;这不是语义级 Prompt Injection 净化。rate limit 也尚未实现。真正生产网关仍需内容隔离、配额、网络出口和消费端的“不可信数据”处理。
第七,工具列表目前对连接可见,真正企业网关可能还要按主体过滤 tools/list,并代理远程 server、刷新能力、处理取消、超时和 schema 版本。发现工具与允许调用是两件事,列表隐藏也不能替代调用时授权。
第八,本实验没有联网、没有真实数据库、没有实际用户流量,因此证据不能升级为 field-tested。后续要公开至少一个固定周期、任务量、P95、审批率、错误率、审计缺口和恢复记录,才能声称真实运行已验证。
商业价值
MCP 的商业价值在于减少每个 AI 客户端与每个数据源之间的重复连接代码;权限网关的价值则来自 让标准连接能够进入企业生产边界。潜在购买者不是需要一个新聊天框的团队,而是已经有工单、CRM、代码仓库、知识库或内部 API,希望 Agent 可以受控读取和执行的组织。
可收费能力更可能包括统一身份映射、租户策略、工具目录、参数级授权、审批队列、秘密代理、审计导出、配额、异常告警和恢复支持。它替代的是每个 Agent 团队重复实现且难以审查的权限胶水,而不是替代现有 IAM、数据库权限或业务审批系统。
商业验收应按任务计算:
每个正确完成任务的净价值
= 自动化节省的人工分钟与响应价值
- 模型、网关、审批和审计成本
- 错误率 × 平均恢复或业务损失
若绝大多数动作都需要人工批准、任务量很低,网关集成成本可能高于节省;若工具本身没有幂等和权限边界,增加 MCP 也不会自动变安全。更适合起步的是高频、可验证、低风险的只读查询,再升级到生成草稿、有限写入和双确认高风险操作。
从作品集角度,这个实验把传统全栈能力迁移到了 AI 系统:API schema 变成工具参数契约,RBAC 变成 principal/tenant/tool policy,数据库事务思维用于一次性审批,日志与可观测性用于 Agent 轨迹,测试则从“返回值正确”升级到“最终业务状态只变化一次”。
复盘
本实验最重要的结果不是写出一个 MCP server,而是确认了三个边界。
第一,协议标准化与业务授权是正交问题。MCP 可以让工具被发现和调用,但“谁能以什么参数改变哪个租户的状态”仍然属于应用系统责任。
第二,模型不应同时持有身份声明、策略解释和审批权。可信身份来自认证层,策略来自本地配置,审批来自独立人工或管理系统;模型只提出动作候选。
第三,安全测试必须检查最终副作用。一个网关可以返回漂亮的拒绝信息,同时已经把请求发给上游;只有上游调用次数、记录版本、另一个租户状态和审计链同时符合预期,才能证明控制真正生效。
下一阶段会把这个基础版本接入 Younis AI Engineering Workbench:增加持久化策略与审批、上游幂等键、超时和取消测试、按主体过滤工具发现,以及可视化审计轨迹。在这些能力实际运行之前,它们只写为计划,不提前升级证据等级。
方法披露
本文使用 AI 工具协助协议资料整理、威胁建模、代码审查和图表草拟;测试由本站本地实际执行,环境、原始结果、失败注入、最终文字与判断由作者逐项复核并负责。
修订记录
2026-07-13:初版发布;固定 MCP SDK 1.29.0,完成 24 个网关测试和双层 MCP 演示;补齐 expiry 关闭、审批多维绑定、版本冲突、参数原文不落日志、未知写入终态、输出校验与终态审计重试证据。
Reusable projects
关联可复用项目
本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。
- 已独立复现
本文逐项重放 MCP 网关的默认拒绝、租户注入、一次性审批、版本冲突和审计故障,并以模拟上游终态验收。
Source ledger
来源账本
以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。
- Introducing the Model Context Protocol - 2024-11-25 snapshot
- Model Context Protocol initial specification snapshot
- MCP initial tool security requirements
- TypeScript SDK v1.29.0 release commit
- InMemoryTransport source at the v1.29.0 release commit
- MCP 2025-03-26 specification changelog snapshot
- Introducing the MCP Registry Preview source snapshot
讨论