MCP 服务端:现状、九家对标与最小能力集

这页分四层:我们现在怎么做(MCP 客户端与认证 stdio 服务端并存,内部 Web 面继续使用独立的 owner 认证)、 别人怎么做references/ 下九个框架逐一,包括两个明确不做的和两个反面案例)、 我们实施了什么(六个工具、默认空白名单、外部低权档位、stdio 启动认证、取消与进度对齐规范)、 理想状态(现有 ACP 的边界、入站 webhook、SDK 各自的差距和为什么本轮不扩张)。 本页是该能力的唯一权威设计与实现证据。

层一 · 我们现在怎么做
层二 · 别人怎么做
层三 · 已实施的最小能力集
层四 · 理想状态
LAYER 1

我们现在怎么做

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。

01方向:MCP 客户端与服务端并存

消费外部工具与向外提供固定工具是两个独立入口;两者现在都已实现,并在 Runtime 注册表处共享受控能力。

MCP 的两个方向 两条实线分别表示出站 MCP 客户端与入站 MCP stdio 服务端。 OpenProgram openprogram/mcp/ ← 客户端,8 个模块 openprogram/mcp_server/ ← 服务端,已实现 工具注册表 _registry 两个方向共用 外部 MCP server drawio、filesystem …… stdio / HTTP / SSE 都支持 已实现 外部 MCP 客户端 Claude Desktop、另一个 agent、IDE 已实现
已有的复用点AgentTool.execute 的签名是 (call_id, args, cancel: asyncio.Event | None, on_update),参数字典、取消事件、增量回调三样协议服务端需要的东西都在。 服务端已通过显式 CallToolRequest handler 将这些参数接到 MCP 1.29.0 stdio transport。

02三个内部面,Web 边界已认证

FastAPI 与 WebSocket 面仍没有版本化承诺,但已由 OwnerAuthMiddleware 保护,并在认证成功后映射为完整 owner authority。该认证不是 MCP client identity,也不是低权限外部协议契约。

调用方 → OwnerAuthMiddleware → 内部路由 所有路径先校验 Host;受保护路径再验证 cookie 或 Bearer。 浏览器 HttpOnly cookie + Origin / CSRF native HTTP / SSE / WS Bearer 必须;Origin 可省略 非 loopback bind 显式 effective Origin + token OwnerAuthMiddleware canonical Host / Origin cookie / Bearer CSRF / no-store 认证后附加 owner authority 198 条 FastAPI 路由 protected by default;内部 shape 不是外部协议承诺 WebSocket /ws · 约 100 个动作 middleware 在 accept() 前认证;约 120 种服务端→客户端事件 工具注册表 _registry register / get / filter_for;三层开关:toolset、expose、defer MCP 不能复用 full-owner Web token:必须有独立 client identity、默认空白名单与低权限 scope
位置规模契约
FastAPI 路由webui/server.pycreate_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。

03审批阶梯:九级,顺序固定

wrap_with_approval 返回一个 execute 被换成 _gated_executeAgentTool 副本。 外部调用方要接的就是这条阶梯,不是绕开它。

_gated_execute 的九级判定 从左到右依次判定,命中即返回。红框先于 bypass,绿框是放行出口。 01 hard constraints 先于规则与 bypass 02 规则 deny / ask deny > ask > allow 03 强制审批工具 bypass 下也询问 04 bypass 短路 Web 聊天默认在此 05 · 06 规则 allow 只读白名单 07 acceptEdits 加路径安全检查 08 auto 分类 LLM 判定风险 09 阻塞式审批卡片 question.asked · once / always 当前默认值分歧: PermissionMode 默认 ask,但 Web 聊天默认 bypass,spawn 出的 sub-agent 在 sub_agent_run.py 里硬编码 bypass。 权限是两档枚举 authority_tier: 门口查 TIER_CAPABILITIES 常量表,请求不带能力列表;档位缺失或未知即拒绝全部能力。外部调用方占来源表最低权一行。

04取消与错误:MCP 边界已统一

CANCELLATION

按轮次 token,两个入口语义不同

openprogram/agent/run_control.py
模型
按轮次的 CancelToken从不按会话threading.Eventretired 标志,轮次结束后到的 stop 不会漏进下一轮。
全局钩子
import 时装 add_pre_invocation_hookset_cancellation_check,每个 @agentic_function 入口和每次 Runtime.exec 都会中止。
入口分歧
WS stop 两段式:优雅停止后 4.0 秒硬杀,并取消待答问题。HTTP /api/stop 单段式,不碰问题注册表。渠道 worker 轮次不注册 token,对两者都不可见。
ERROR

is_error 三处均为一等字段

functions/_runtime.py · agent/types.py
工具层
ToolResult 有一等的 is_error: bool
框架层
AgentToolResult 已有一等的 is_error: bool;Runtime、approval、agent loop 与 MCP 转换均读取该字段,details 只保留诊断信息。
事件层
AgentEventToolEnd 有一等的 is_error
具体载荷
is_error 是顶层 typed bit;details 只保留固定 reason_codedeniedtimeout 等诊断字段。
已有契约测试tests/unit/test_diagnostic_mcp_route_contracts.py 钉住 /api/doctor{"results", "all_ok"}/api/mcp/servers 的 15 键 status 字典、 以及缺失 server 的 404 形状。三个测试都新建裸 FastAPI()register(app),所以 OwnerAuthMiddleware 不在路径上;断言用精确相等,加字段就挂。
LAYER 2

别人怎么做

references/ 下九个框架逐一核实。四家做 MCP 服务端,三家做 ACP 服务端, 两家明确不做(pi-mono 写着 "No MCP.",pi-ai 本身是 provider 库)。 两个反面案例:weclaw 的 /api/send 零认证且 ACP 权限自动放行,opencode 的 serve 未设密码时只警告不阻断。

05九家对标矩阵

框架MCP 服务端ACP(agent 侧)入站 HTTP / webhook客户端 SDK
claude-code无(快照只有 BashTool/)
claude-code-leakedclaude mcp serve,stdio,全部内置工具,空权限上下文claude server,自动生成 sk-ant-cc-* bearerAgent SDK 走 control protocol
codex-clicodex mcp-server,stdio,只有 codex / codex-reply无,用自己的 app-serverapp-server:stdio / WS / unix,token 文件 / SHA-256 / JWTTS @openai/codex-sdk + Python
openclaw三个 stdio server 加 MCP-over-HTTP(bearer + loopback)openclaw acp,走 GatewayGateway HTTP API + HMAC webhooks 插件@openclaw/sdk(private)
opencode无 —— 只做客户端opencode acpopencode serve,HTTP Basic;未设密码只警告@opencode-ai/sdk,由 OpenAPI 生成
hermes-agenthermes 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零认证
OpenProgramopenprogram mcp serve,认证 stdio,六个固定 wrapper,默认空白名单openprogram acp,stdio;已有会话、权限与事件映射内部 Web 路由,owner token/cookie 认证无专属 SDK;使用标准 MCP SDK

06四种协议写法,各自的取舍

协议内审批 · 推

codex:elicitation/create

exec_approval.rs · patch_approval.rs
机制
exec 与 patch 审批推回 MCP 客户端,带自定义关联字段 threadIdcodex_elicitationcodex_mcp_tool_call_idcodex_event_id,收回 ReviewDecision
取舍
延迟低,但依赖客户端支持 elicitation;源码自注该载荷还不符合 ElicitResult(缺 action / content)。
协议内审批 · 拉

openclaw / hermes:轮询一对工具

permissions_list_open · permissions_respond
机制
把待审批当数据暴露成两个工具,决策 allow-once | allow-always | deny。openclaw 另外通过实验能力位 claude/channel/permission 支持推送。
取舍
不支持 elicitation 的通用客户端也能用,降级平滑;代价是多一次往返与轮询间隔。
取消

只有 codex 做全

notifications/cancelled · active_turn_registry.rs
codex
按请求 id 在活跃轮次注册表里查到该轮并停止向它路由事件;tasks/cancel 明确答不支持。
openclaw
plugin-tools server 把 SDK 的 extra.signal 转进 callTool(params, signal)
其余
claude-code-leaked 每次调用新建 AbortController从不接到通知上;hermes 没有取消。
进度与错误

进度没人用,错误已收敛

notifications/progress · isError
进度
codex 收到 notifications/progress 只记日志,自己的流式是自定义通知形状。被调查的框架没有一个对外发标准 progress
错误
全部对"工具跑了但失败"返回 {isError: true, content:[text]},把 JSON-RPC 错误留给协议级故障(工具名不存在、参数格式错)。这个划分我们采纳。
两个反面案例:weclaw 的 POST /api/send 没有任何认证,且其 ACP 客户端对每个 session/request_permission 无条件自动放行;opencode 的 serveOPENCODE_SERVER_PASSWORD 未设时以无认证运行,只打印一行警告。 claude-code-leaked 的 mcp serve 用空权限上下文暴露全部内置工具,且 isNonInteractiveSession: true, 根本没有交互式询问路径。这三种姿态都被本轮验收标准排除。
LAYER 3

已实施的最小能力集

openprogram mcp serve 走 stdio 单一传输,暴露六个工具, tool_call 按默认为空的白名单过滤,外部调用方拿 paired 低权档位且永不 interactive, 启动时通过环境凭证完成认证且没有关闭开关,取消映射到已有的按轮次 token,进度由 on_update 驱动,错误按上面那个划分。以下内容均已绑定到实现提交 885d8f15

07六个工具,每个映射一项能力

codex 两个、hermes 十个、openclaw 九个都活得很好;claude-code-leaked 暴露全部工具的那种做法是要避开的。我们取六个。

工具作用能力宿主副作用
sessions_list列出会话的 id、标题、更新时间reply
session_get取一个会话的消息reply
prompt_send在会话里开一轮并返回结果(会话 id 可选)reply仅追加会话
prompt_cancel取消本调用方开的在途轮次reply
tools_list列出调用方档位允许的工具及 schemareply
tool_call按名调用一个工具按工具而定走白名单加审批阶梯
没有的东西:没有 register 的等价物,没有配置改写,没有凭证访问,也不直接暴露文件工具。 想读文件的调用方用文件工具名去调 tool_call,一样过白名单和审批阶梯。 prompt_send 是把 codex 的 codex/codex-reply 收成一次调用。

08白名单在前,档位在中,阶梯在后

三道关串联,任何一道不过就不执行。白名单是过滤器,不是审批的替代品。

一次外部 tool_call 必须依次通过三道关 向下的红色虚线是各道关的拒绝出口,每种拒绝对应一种协议响应。 外部 MCP 客户端 tool_call(name, args) 进程启动时已验证环境 token 关一 · 暴露白名单 mcp_server.exposed_tools 默认为空,owner 点名才可达 关二 · authority_tier paired 档位 ∩ 暴露白名单 interaction 永不为 interactive 关三 · 现有审批阶梯 permission_mode 固定 ask,不可按请求配置 无本地 owner 在场时需审批的调用判为拒绝 JSON-RPC 错误,按工具名不存在 调用方得不到关于无权工具的任何信息 isError: true 正文点名缺失的能力 isError: true,带拒绝原因 不静默放行,也不永久阻塞 source=mcp 属于 non-interactive;hard constraints 先于三道关,风险工具与目录外写入一律拒绝,任何问题不得阻塞等待

外部调用方在 沙箱设计的请求来源表里占最低权的一行:

请求来源principalauthority_tier缺失字段处理
认证本地 Web / CLI / TUIprincipal_id=ownerinteraction=interactiveowner,持有 approval.request入口必须主动构造;认证无效即拒绝
已配对渠道账号实例 owner,speaker 另存paired,持有 replymemory.source.append不带档位的消息直接拒绝,不做降档
外部 MCP 客户端实例 owner,客户端 id 另存paired 再与暴露工具白名单取交集身份缺失或未通过校验即拒绝连接
continuation / subagent显式继承 owner原样继承调用方档位,永不扩权缺字段即状态错误,deny
cron 触发创建时批准的 owner批准时固化的 job capability触发时不重算为 interactive

09认证:stdio 启动时验证,没有协议内 token

TOKEN

生成一次,常量时间比较

<state_dir>/mcp_server_token(0600) ↔ OPENPROGRAM_MCP_TOKEN
生成
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、日志、错误或会话内容。
权限语义
环境凭证只允许建立低权限 stdio 连接;所有请求固定映射为 paired,不会获得 owner authority。
TRANSPORT

stdio 单一传输

openprogram mcp serve 模块 openprogram/mcp_server/
为什么
被调查的每个实现都最先支持 stdio;它继承拉起进程那方的信任边界;而且不把内部 full-owner Web API 暴露成 integration surface。
规范基线
首版锁定仓库当前 mcp 1.29.0 与协议 2025-11-25;由 SDK 协商兼容版本,不自行扩展握手字段。
HTTP
等 stdio contract 稳定后单独设计 Streamable HTTP、OAuth、Origin 校验与部署生命周期,不复用 Web owner token。
模块位置
openprogram/mcp/ 平级的顶层模块,不复用客户端模块(翻译方向相反),但共用 _runtime.py 与审批阶梯 —— 共用这两样正是全部意义。

10取消、进度、错误对齐 MCP 规范

协议消息映射到已有机制,不新造机制 notifications/cancelled 按 MCP 请求 id 查 session_id 与 token 已有的按轮次 CancelToken 停止后续副作用,清理 active request 与待答问题,并记录审计 execute 的 cancel 参数 wire cancellation 设置同一个请求级事件 notifications/progress 仅当客户端给了 progressToken 才发 execute 的 on_update 回调 轮次级:工具开始、工具结束,不做 token 级流式 没给 token 就丢弃回调 进度是优化,从不是正确性要求
情况响应理由
工具名不存在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 resultSDK 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 完成 initializeclientInfo 的任意自报值不改变权限; 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 的补充检查。
LAYER 4

理想状态

ACP 已有 stdio 服务端入口,本轮只保持现状而不扩张协议面;入站 webhook 缺固化的 job capability, SDK 缺一个稳定到可以承诺的面。后两项只有在前置安全与兼容条件具备后才进入设计。

11三处差距,以及现在不做的理由

EXISTING · OUT OF SCOPE

现有 ACP stdio 服务端

openprogram acp 已通过 openprogram/acp/server.py 提供 stdio 入口, 并已有会话、权限请求与事件映射。它当前使用 owner authority 与交互式权限语义;这与本设计的独立 MCP client identity、 固定 paired scope 和 non-interactive gate 是两个不同入口。
本轮不扩张:不改 ACP transport、协议能力或授权语义,不把 MCP 的 token、白名单或工具面并入 ACP。 只在共享 Runtime 类型修订时运行现有 ACP 回归测试。
DEFERRED · WEBHOOK

入站 webhook

hermes 那版说明了正确性的代价:按路由的 HMAC secret 且启动时校验、按路由限流、 防重投的幂等缓存、读取前检查的 body 大小限制。这是地板不是加分项。 差距在于 webhook 是推式触发,到达时没有 owner 在场,这把它归到跟 cron 触发同一类: 它需要注册时批准并固化的 job capability,而不是投递时算出来的档位。
现在不做:前置条件是沙箱批次 I 与第 05B 步造的固化 job capability,排在另一条线上。 而 MCP 覆盖了拉式集成,那是我们真正能点名的需求。
DEFERRED · SDK

客户端 SDK

codex 出 TS 和 Python,opencode 由 OpenAPI 生成 TS,openclaw 出 private TS 包, claude-code-leaked 出 Agent SDK。hermes 一个不出,让集成方直接用 MCP / ACP / HTTP。 差距在于 SDK 是一份兼容承诺,而 198 条路由加约 100 个动作是随前端变化的内部面,只有两条钉了形状。
现在不做:顺序刻意 —— 先做 MCP,因为对每种有 SDK 的语言来说 MCP SDK 就是那个客户端库, 这正是 hermes 零 SDK 还能被集成的原因。专属 SDK 只有在 MCP 工具面被证明太粗时才配存在。
稳定面在哪里:本轮只发布上述六个 MCP 工具;现有 ACP 保持原契约,webhook 与专属 SDK 继续不发布。 FastAPI 和 WebSocket 仍是内部面,不成为 MCP transport,也不增加 HTTP、OAuth 或 Web route。

12实现状态

层三已在实现提交 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_listsession_getprompt_sendprompt_canceltools_listtool_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已实施固定 pairedspeaker_kind=clientinteraction=non-interactiveclientInfo 不参与认证或授权
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/progresson_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.pyopenprogram/acp/server.py;只做共享类型回归
入站 webhook本轮不做依赖沙箱批次 I 与第 05B 步的固化 job capability
客户端 SDK本轮不做标准 MCP SDK 已覆盖客户端角色;仅当已冻结的六工具面被真实集成证明过粗时,再考虑专属 OpenProgram SDK
Reviewed implementation evidence:实现基线 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 brokengit diff --check PASS。 编辑后以相同五条命令复验:MCP server focused 195 passed;共享回归 198 passed; 文档构建 453 pages;链接检查 0 brokengit diff --check PASS。
明确排除:本次实现没有 HTTP、SSE、Streamable HTTP、OAuth、Web route、Web owner token 复用、 ACP transport/contract 扩张、专属客户端 SDK 或 MCP SDK 依赖范围扩张。对外协议面仍只有认证 stdio 与上述六个 wrapper。
当前 Web 边界不是 MCP server 条目OwnerAuthMiddleware 已保护 singleton owner 的 HTTP、SSE 与 WebSocket, 但它不认证 external MCP client、不执行 tool whitelist,也不分配这里定义的低权限 scope。独立 MCP server 已实现这些控制且不复用 Web owner token。