AI 工程实践

MCP 权限网关可复现实验:身份、租户、一次性审批与审计

使用官方 @modelcontextprotocol/sdk 1.29.0 建立 Client、权限 Gateway 与上游 Server 两段连接,以 24 个本地测试验证默认拒绝、租户注入、审批防重放、版本冲突、参数值不以原文落日志、写前审计故障关闭与未知写入终态对账。

发布:2026/07/13更新:2026/07/13
MCP 测试客户端经过身份和策略、一次性审批、审计三道门后才能调用租户隔离的上游工具
图 1:本站可复现实验的两段 MCP 连接与三道硬门;最终结论以模拟上游调用次数和状态变化为准,不以模型回答为准。

时间与证据

这是一项 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 中自报的 userIdclientIdtenantId 一律不可信。

证据等级标为 reproduced,只覆盖仓库内实际运行的 24 个 MCP 网关测试与一个演示流程。它不代表生产 OAuth、分布式事务、长期流量或真实攻击已经验证。

实验设计

两段连接,不让 Gateway 直接伪造成功

实验建立三个角色和两段官方 SDK 连接:

测试 Client
  -> InMemoryTransport(注入已验证 AuthInfo 的测试夹具)
  -> Gateway McpServer
  -> 内部 Client + 第二段 InMemoryTransport
  -> Mock Upstream McpServer
  -> tenant-a / tenant-b 权威状态

Gateway 对外只暴露 records.readrecords.update。上游存储暴露独立的 storage.readstorage.update。Gateway 必须先完成身份、resource audience、scope、本地策略和审批检查,成功后才把可信租户注入上游调用。测试最后读取上游调用数组、记录版本和两个租户的状态,避免把“返回了一段拒绝文字”误当成“写操作真的没有发生”。

MCP/SDK 原生能力与本站实现必须分开

层次 本实验实际使用的能力 不能误写成什么
MCP/SDK 初始化、tools/listtools/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 中的 readOnlyHintdestructiveHint 只是提示。实验虽然为两个工具设置了 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 再记录 decisionoutcome,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,审计记录 unknownreconciliationRequired=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:test27/27 通过。测试不是随机采样,每次运行都从固定内存状态开始。原始 TAP、环境和确定性演示输出分别保存在 labs/mcp-policy-gateway/results/2026-07-13-node-test.tap2026-07-13-environment.txt2026-07-13-demo.json,不只保留本文摘要;其中最后一个测试会实际运行演示并逐字段比对仓库中的 JSON 工件。

验证组 实际断言 权威结果
正常只读 Alice 读取同名 rec_shared 返回 tenant-a 版本 1;没有读到 tenant-b 版本 4
身份关闭 无身份、过期/零/缺失/非有限 expiry、错误 resource、缺 scope 全部 isError=true;上游调用数 0
参数与伪造身份 args 增加 tenantId=tenant-bprincipal=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

关联可复用项目

本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。

Source ledger

来源账本

以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。

  1. Introducing the Model Context Protocol - 2024-11-25 snapshot
    Anthropic(Internet Archive snapshot)历史页面存档文章资料来源发布:2024/11/25本站核验:2026/07/13
  2. Model Context Protocol initial specification snapshot
    Model Context Protocol官方仓库文章资料来源发布:2024/11/24本站核验:2026/07/13
  3. MCP initial tool security requirements
    Model Context Protocol官方文档文章资料来源发布:2024/11/24本站核验:2026/07/13
  4. TypeScript SDK v1.29.0 release commit
    Model Context Protocol官方仓库文章资料来源发布:2026/03/30本站核验:2026/07/13
  5. InMemoryTransport source at the v1.29.0 release commit
    Model Context Protocol官方仓库文章资料来源发布:2026/03/30本站核验:2026/07/13
  6. MCP 2025-03-26 specification changelog snapshot
    Model Context Protocol官方文档文章资料来源发布:2025/03/26本站核验:2026/07/13
  7. Introducing the MCP Registry Preview source snapshot
    Model Context Protocol官方文档文章资料来源发布:2025/09/08本站核验:2026/07/13

讨论

正在加载评论...