这页分四层:我们现在怎么做(MCP 客户端与认证 stdio 服务端并存,内部 Web 面继续使用独立的 owner 认证)、
别人怎么做(references/ 下九个框架逐一,包括两个明确不做的和两个反面案例)、
我们实施了什么(六个工具、默认空白名单、外部低权档位、stdio 启动认证、取消与进度对齐规范)、
理想状态(现有 ACP 的边界、入站 webhook、SDK 各自的差距和为什么本轮不扩张)。
本页是该能力的唯一权威设计与实现证据。
MCP 客户端与认证 stdio 服务端现已并存。内部控制面仍是 198 条 FastAPI 路由加约 100 个 WebSocket 动作,
由 OwnerAuthMiddleware 统一执行 process token、cookie/Bearer、Host/Origin 与 accept 前 WebSocket 认证。
MCP server 另用独立 0600 token、凭证指纹 client identity、固定 paired scope、默认空白名单与 non-interactive gate;不复用 Web owner token。
消费外部工具与向外提供固定工具是两个独立入口;两者现在都已实现,并在 Runtime 注册表处共享受控能力。
AgentTool.execute 的签名是
(call_id, args, cancel: asyncio.Event | None, on_update),参数字典、取消事件、增量回调三样协议服务端需要的东西都在。
服务端已通过显式 CallToolRequest handler 将这些参数接到 MCP 1.29.0 stdio transport。FastAPI 与 WebSocket 面仍没有版本化承诺,但已由 OwnerAuthMiddleware 保护,并在认证成功后映射为完整 owner authority。该认证不是 MCP client identity,也不是低权限外部协议契约。
| 面 | 位置 | 规模 | 契约 |
|---|---|---|---|
| FastAPI 路由 | webui/server.py 的 create_app(),路由在 webui/routes/ | 198 条 / 约 25 个模块 | 2 条有精确相等断言 |
WebSocket /ws | _websocket_handler,动作来自 webui/ws_actions/ | 约 100 个动作,约 120 种事件 | 无 |
| 工具注册表 | functions/_runtime.py 的 _registry | 按工具名索引 | 无外部契约 |
OwnerAuthMiddleware 已保护 Web 的 HTTP、SSE 与 WebSocket,
但认证结果是 singleton owner 的完整 authority。MCP server 必须单独认证 external client、执行默认空白名单并分配低权限 scope,
不能向 client 提供或复用 Web owner token。wrap_with_approval 返回一个 execute 被换成 _gated_execute 的 AgentTool 副本。
外部调用方要接的就是这条阶梯,不是绕开它。
CancelToken,从不按会话:threading.Event 加 retired 标志,轮次结束后到的 stop 不会漏进下一轮。add_pre_invocation_hook 与 set_cancellation_check,每个 @agentic_function 入口和每次 Runtime.exec 都会中止。stop 两段式:优雅停止后 4.0 秒硬杀,并取消待答问题。HTTP /api/stop 单段式,不碰问题注册表。渠道 worker 轮次不注册 token,对两者都不可见。ToolResult 有一等的 is_error: bool。AgentToolResult 已有一等的 is_error: bool;Runtime、approval、agent loop 与 MCP 转换均读取该字段,details 只保留诊断信息。AgentEventToolEnd 有一等的 is_error。is_error 是顶层 typed bit;details 只保留固定 reason_code、denied、timeout 等诊断字段。tests/unit/test_diagnostic_mcp_route_contracts.py 钉住
/api/doctor 的 {"results", "all_ok"}、/api/mcp/servers 的 15 键 status 字典、
以及缺失 server 的 404 形状。三个测试都新建裸 FastAPI() 再 register(app),所以 OwnerAuthMiddleware 不在路径上;断言用精确相等,加字段就挂。references/ 下九个框架逐一核实。四家做 MCP 服务端,三家做 ACP 服务端,
两家明确不做(pi-mono 写着 "No MCP.",pi-ai 本身是 provider 库)。
两个反面案例:weclaw 的 /api/send 零认证且 ACP 权限自动放行,opencode 的 serve 未设密码时只警告不阻断。
| 框架 | MCP 服务端 | ACP(agent 侧) | 入站 HTTP / webhook | 客户端 SDK |
|---|---|---|---|---|
| claude-code | 无(快照只有 BashTool/) | 无 | 无 | 无 |
| claude-code-leaked | claude mcp serve,stdio,全部内置工具,空权限上下文 | 无 | claude server,自动生成 sk-ant-cc-* bearer | Agent SDK 走 control protocol |
| codex-cli | codex mcp-server,stdio,只有 codex / codex-reply | 无,用自己的 app-server | app-server:stdio / WS / unix,token 文件 / SHA-256 / JWT | TS @openai/codex-sdk + Python |
| openclaw | 三个 stdio server 加 MCP-over-HTTP(bearer + loopback) | openclaw acp,走 Gateway | Gateway HTTP API + HMAC webhooks 插件 | @openclaw/sdk(private) |
| opencode | 无 —— 只做客户端 | opencode acp | opencode serve,HTTP Basic;未设密码只警告 | @opencode-ai/sdk,由 OpenAPI 生成 |
| hermes-agent | hermes mcp serve,FastMCP stdio,十个工具 | hermes acp 加 ACP 注册表条目 | HMAC webhook 适配器 + bearer OpenAI 兼容 API | 无 —— 让集成方直接用 MCP / ACP / HTTP |
| pi-ai | 无 | 无 | 无 | 不适用,本身是 provider 客户端库 |
| pi-mono | 无,README 明写 "No MCP." | 无 | 无,只有进程内 pi.send() | 只有内嵌用的包 |
| weclaw | 无 | 只有 ACP 客户端,自动放行所有权限请求 | POST /api/send,零认证 | 无 |
| OpenProgram | openprogram mcp serve,认证 stdio,六个固定 wrapper,默认空白名单 | openprogram acp,stdio;已有会话、权限与事件映射 | 内部 Web 路由,owner token/cookie 认证 | 无专属 SDK;使用标准 MCP SDK |
threadId、codex_elicitation、codex_mcp_tool_call_id、codex_event_id,收回 ReviewDecision。ElicitResult(缺 action / content)。allow-once | allow-always | deny。openclaw 另外通过实验能力位 claude/channel/permission 支持推送。tasks/cancel 明确答不支持。extra.signal 转进 callTool(params, signal)。AbortController 但从不接到通知上;hermes 没有取消。notifications/progress 只记日志,自己的流式是自定义通知形状。被调查的框架没有一个对外发标准 progress。{isError: true, content:[text]},把 JSON-RPC 错误留给协议级故障(工具名不存在、参数格式错)。这个划分我们采纳。POST /api/send 没有任何认证,且其 ACP 客户端对每个
session/request_permission 无条件自动放行;opencode 的 serve 在
OPENCODE_SERVER_PASSWORD 未设时以无认证运行,只打印一行警告。
claude-code-leaked 的 mcp serve 用空权限上下文暴露全部内置工具,且 isNonInteractiveSession: true,
根本没有交互式询问路径。这三种姿态都被本轮验收标准排除。openprogram mcp serve 走 stdio 单一传输,暴露六个工具,
tool_call 按默认为空的白名单过滤,外部调用方拿 paired 低权档位且永不 interactive,
启动时通过环境凭证完成认证且没有关闭开关,取消映射到已有的按轮次 token,进度由 on_update 驱动,错误按上面那个划分。以下内容均已绑定到实现提交 885d8f15。
codex 两个、hermes 十个、openclaw 九个都活得很好;claude-code-leaked 暴露全部工具的那种做法是要避开的。我们取六个。
| 工具 | 作用 | 能力 | 宿主副作用 |
|---|---|---|---|
sessions_list | 列出会话的 id、标题、更新时间 | reply | 无 |
session_get | 取一个会话的消息 | reply | 无 |
prompt_send | 在会话里开一轮并返回结果(会话 id 可选) | reply | 仅追加会话 |
prompt_cancel | 取消本调用方开的在途轮次 | reply | 无 |
tools_list | 列出调用方档位允许的工具及 schema | reply | 无 |
tool_call | 按名调用一个工具 | 按工具而定 | 走白名单加审批阶梯 |
register 的等价物,没有配置改写,没有凭证访问,也不直接暴露文件工具。
想读文件的调用方用文件工具名去调 tool_call,一样过白名单和审批阶梯。
prompt_send 是把 codex 的 codex/codex-reply 收成一次调用。三道关串联,任何一道不过就不执行。白名单是过滤器,不是审批的替代品。
外部调用方在 沙箱设计的请求来源表里占最低权的一行:
| 请求来源 | principal | authority_tier | 缺失字段处理 |
|---|---|---|---|
| 认证本地 Web / CLI / TUI | principal_id=owner,interaction=interactive | owner,持有 approval.request | 入口必须主动构造;认证无效即拒绝 |
| 已配对渠道账号 | 实例 owner,speaker 另存 | paired,持有 reply 与 memory.source.append | 不带档位的消息直接拒绝,不做降档 |
| 外部 MCP 客户端 | 实例 owner,客户端 id 另存 | paired 再与暴露工具白名单取交集 | 身份缺失或未通过校验即拒绝连接 |
| continuation / subagent | 显式继承 owner | 原样继承调用方档位,永不扩权 | 缺字段即状态错误,deny |
| cron 触发 | 创建时批准的 owner | 批准时固化的 job capability | 触发时不重算为 interactive |
openprogram mcp token create 原子写入 token 文件并只打印一次;serve 不隐式生成 token。openprogram mcp serve 在读取任何 MCP 消息前,用常量时间比较环境变量与 token 文件;缺失、权限错误或不匹配均以非零状态退出,stderr 固定为 Error: MCP server authentication failed,stdout 为空。initialize.clientInfo 当前不被读取或记录,也不参与认证、身份、authority、routing 或结果;token 不进入 JSON-RPC、日志、错误或会话内容。paired,不会获得 owner authority。mcp 1.29.0 与协议 2025-11-25;由 SDK 协商兼容版本,不自行扩展握手字段。openprogram/mcp/ 平级的顶层模块,不复用客户端模块(翻译方向相反),但共用 _runtime.py 与审批阶梯 —— 共用这两样正是全部意义。| 情况 | 响应 | 理由 |
|---|---|---|
| 工具名不存在 | JSON-RPC 错误,方法级 | 协议级故障 |
| 参数不过 schema 校验 | JSON-RPC 错误,invalid params | 协议级故障 |
| 启动环境 token 缺失或错误 | 进程非零退出,不进入 MCP 生命周期 | 认证发生在读取 stdin 之前,错误只写 stderr |
| 工具不在白名单里 | JSON-RPC 错误,按工具名不存在处理 | 不泄露无权工具的存在 |
| 档位不含该能力 | isError: true,正文点名缺失能力 | 工具存在但本次不可用 |
| 审批被拒或无法审批 | isError: true,仅保留固定 gate reason | 无 owner 在场时判为拒绝,不透传任意工具 details |
| 工具跑了并失败 | isError: true,固定净化正文与 reason code | 保留 typed error,不跨边界泄漏异常、trace 或工具自报原因 |
| 外部 SDK 取消在途请求 | ErrorData(code=0, message="Request cancelled", data=None);应用不返回 tool result | SDK 1.29.0 的 notifications/cancelled 拥有响应;OpenProgram 只做停止、清理与审计 |
普通 prompt_cancel | 同连接活跃轮次返回 cancelled=true;foreign/completed 返回 false | 它是六个 wrapper 之一,不等同于 SDK cancellation notification |
is_error 已提升为 AgentToolResult 的一等字段;
Runtime normalization、approval refusal、agent loop、MCP client/server conversion 与 cache eligibility 均使用该 typed bit。
旧 details["is_error"] 仅保留输入兼容桥,不再是仓库内生产者的 canonical 状态。tests/integration/test_mcp_server.py 以子进程拉起
openprogram mcp serve,用 MCP SDK 自己的客户端连上去,覆盖以下协议与安全行为:
无 token、错 token 时子进程在握手前失败,正确环境 token 时 SDK 完成 initialize;clientInfo 的任意自报值不改变权限;
MCP tools/list 精确返回六个固定入口,而其中的 tools_list 结果精确等于配置白名单、当前注册表与 paired scope 的交集;
交集内只读 tool_call 返回内容;未知或未暴露工具按不存在处理;已暴露但 scope 越权及需审批的调用返回 isError 而不是挂住;
notifications/cancelled 使 SDK 为原 prompt_send 返回标准 JSON-RPC cancellation error,断言不存在后续 tool result / isError;
同时验证后续副作用停止、活跃请求与问题状态清空、审计事件落下。进度通知当且仅当给了 progressToken 时到达,保持每请求顺序;
正常、错误与取消路径均有界终止 progress consumer,外部 parent cancellation 不被 teardown 吞掉。
验收使用真实 MCP 1.29.0 ClientSession 经真实 openprogram mcp serve 子进程覆盖六个 wrapper、typed errors、并发、进度、取消与 stdout 纯协议帧;手写帧只作为原始 stdout 的补充检查。ACP 已有 stdio 服务端入口,本轮只保持现状而不扩张协议面;入站 webhook 缺固化的 job capability, SDK 缺一个稳定到可以承诺的面。后两项只有在前置安全与兼容条件具备后才进入设计。
openprogram acp 已通过 openprogram/acp/server.py 提供 stdio 入口,
并已有会话、权限请求与事件映射。它当前使用 owner authority 与交互式权限语义;这与本设计的独立 MCP client identity、
固定 paired scope 和 non-interactive gate 是两个不同入口。层三已在实现提交 885d8f15 完成,并经独立 specification 与 quality review 收敛为零 remaining finding。MCP server 与现有 MCP client、ACP stdio、内部 Web surface 分属独立入口和认证边界。
| 项 | 状态 | 实现与证据 |
|---|---|---|
openprogram mcp serve stdio 服务端 | 已实施 | openprogram/mcp_server/server.py;认证在进入 SDK stdio 前完成 |
| 六工具最小集 | 已实施 | sessions_list、session_get、prompt_send、prompt_cancel、tools_list、tool_call;SDK tools/list 精确返回这六项 |
mcp_server.exposed_tools 白名单,默认为空 | 已实施 | discovery 与 execution 每次取 configured allowlist ∩ live registry ∩ paired capability |
mcp token create + stdio 启动环境认证,无关闭开关 | 已实施 | 独立 0600 token;OPENPROGRAM_MCP_TOKEN 常量时间比较;身份为凭证 SHA-256 前 16 位小写十六进制 |
外部调用方的 authority_tier 行 | 已实施 | 固定 paired、speaker_kind=client、interaction=non-interactive;clientInfo 不参与认证或授权 |
source=mcp non-interactive + hard-constraint gate | 已实施 | 危险 process/worktree/out-of-root write 先硬拒绝;审批与一般问题不等待,拒绝结果 typed 且净化 |
| SDK cancellation → 请求清理与审计 | 已实施 | 精确 request/session/connection ownership;设置 thread/tool event,清理 process、Runtime、questions、run-control,再记录 mcp.request.cancelled |
notifications/progress 由 on_update 驱动 | 已实施 | 仅有 progressToken 时发送;每请求单调有序;normal/error/cancel 均有界清理 consumer,保留 parent cancellation |
AgentToolResult 上的一等 is_error | 已实施 | Runtime 失败、拒绝与 wire CallToolResult.isError 端到端使用 typed bit;敏感正文/details 在 MCP 边界净化 |
| 端到端协议测试 | 已实施 | 真实 MCP 1.29.0 ClientSession + CLI stdio 子进程,协议 2025-11-25;覆盖六项、clientInfo invariance、typed errors、question decline、progress、并发、取消、审计与 stdout 隔离 |
| ACP stdio 服务端 | 已存在;本轮不扩张 | openprogram/_cli_cmds/acp.py 与 openprogram/acp/server.py;只做共享类型回归 |
| 入站 webhook | 本轮不做 | 依赖沙箱批次 I 与第 05B 步的固化 job capability |
| 客户端 SDK | 本轮不做 | 标准 MCP SDK 已覆盖客户端角色;仅当已冻结的六工具面被真实集成证明过粗时,再考虑专属 OpenProgram SDK |
885d8f15;Task 8 最终 specification review
PASS,0 remaining findings,quality review PASS,0 remaining findings。Task 9 编辑前五项 release gate:MCP server focused
195 passed;共享 authority / permission / hard-constraint / questions / ACP / Runtime 回归 198 passed;
文档构建 453 pages;链接检查 0 broken;git diff --check PASS。
编辑后以相同五条命令复验:MCP server focused 195 passed;共享回归 198 passed;
文档构建 453 pages;链接检查 0 broken;git diff --check PASS。OwnerAuthMiddleware 已保护 singleton owner 的 HTTP、SSE 与 WebSocket,
但它不认证 external MCP client、不执行 tool whitelist,也不分配这里定义的低权限 scope。独立 MCP server 已实现这些控制且不复用 Web owner token。