关于 Provared MCP executor
Provared MCP executor 位于 Agent 与 MCP 工具服务之间,供需要权限检查和调用记录的场景使用。它在转发调用前验证通行密钥签署的权限,并为回执添加签名;记录仅涵盖经由它的调用。
主要功能
- 01检查通行密钥签署的权限
- 02按规则映射工具、操作与金额
- 03保存调用副本并签署回执
- 04为不允许的调用写入拒绝记录
- 05支持 stdio 与 Streamable HTTP
适用场景
- 01限制 Agent 只能执行获准操作
- 02记录并审查工具调用结果
- 03通过 HTTP 服务多个 Agent
- 04将回执与 Agent 日志进行比对
MCP 相关
它可在 stdio 模式下充当 MCP 代理,也可作为 Streamable HTTP MCP 服务置于上游 HTTP MCP 服务之前。它会隐藏未配置规则的工具,并在转发每次 tools/call 前进行检查。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
provared-mcp-executor
这是执行器记录的 Agent 通过它发出的请求,以及执行器对每次调用的处理结果。它无法证明未经过它的调用。 只有当 Agent 无法通过其他方式访问它后面的工具服务时,这份记录才算数:收回 Agent 持有的服务凭据,让凭据仅由执行器持有。
根据互联网草案
draft-izmaylov-agent-permission-receipts,执行器是指 Agent 之外的软件,它代表 Agent 执行操作或持有执行操作所需的内容,并拒绝执行权限不允许的操作。这个执行器位于 Agent 和使用模型上下文协议(MCP)的工具服务之间。它有两种运行方式:通过 stdio 传输时,Agent 的 MCP 客户端会像启动工具服务一样启动执行器,而执行器则携带服务凭据启动真正的服务;通过 HTTP 时,执行器本身是一个 Streamable HTTP MCP 服务,位于一个上游 HTTP MCP 服务前面,多个 Agent 可调用它,每个 Agent 都使用自己的秘密进行身份验证(参见“通过 HTTP 为多个 Agent 提供服务”)。无论哪种方式,对于每个 tools/call:
- 根据用户编写的规则文件,通过工具名称确定调用对应的操作及其金额,而不是依据 Agent 提供的信息;
- 向 Agent 的存根签署器询问用户签署的权限(凭条)是否允许该操作:这是 Provared 库在执行操作前进行的检查;
- 如果允许,则将调用副本写入磁盘,转发调用,并写入已签名的回执(存根),其中注明执行器,并通过指纹绑定参数和服务的响应,且使用执行器自己的密钥共同签署;
- 如果不允许,则不发送任何内容,通过工具错误告知模型原因,并写入已签名的拒绝记录。
所有其他消息(server/discover、initialize、ping、资源、提示词、通知,以及旧版服务向客户端发出的请求)均会原样通过。tools/list 的响应中不会包含没有规则的工具。
执行器持有 Agent 的密钥和服务凭据。Agent 两者都不持有,因此无法编写、修改或跳过回执,前提是它无法读取执行器的文件:如果拥有 shell 或文件工具的 Agent 以同一计算机用户身份运行,就能读取这些文件,从而接触到副本、密钥和凭据。请以 Agent 无法使用的独立用户账户运行执行器,或者不要向 Agent 提供 shell 或文件工具。通过 HTTP 运行时,也可以将执行器与 Agent 分开部署,让 Agent 无法读取执行器的文件:
一个示例将它们放在同一台计算机上的不同 Docker 容器中,并已在 Windows 上通过 Docker Desktop 完成全部七项检查;尚未在不同计算机上测试。执行器还会独立于 Agent 的日志,保存每次调用的副本,可将副本与日志进行比对,以查找遗漏的回执(compare)。
这是证据,不是裁决。完整设计及供审查的编号选项见 DESIGN.md。格式说明见 Provared 格式描述(仓库中的 spec/provared-format.md);所用库为 npm 包 provared。
安装
npm install -g provared-mcp-executor
也可以从仓库副本安装:在 mcp-executor 中运行 npm pack,然后运行 npm install -g ./provared-mcp-executor-0.3.0.tgz。
需要 Node.js 24.7 或更高版本,并且只依赖 provared 库,版本为 0.7.0 或更高的 0.7 版本(^0.7.0):其回执会使用 provared.tool-name 和 provared.tool-result,这两个名称在格式 0.7.0 中被保留,而 0.5.0 或 0.6.0 的检查器会拒绝它们(0.4.0 及更早版本会将它们作为标签读取)。
设置
-
编写设置(
executor.example.json可作为起点):要启动的服务,以及从哪里读取其凭据;Agent 的密钥、许可和账本路径;执行器的密钥和副本;以及工具和各自对应的操作。路径以设置文件所在文件夹为基准读取。 -
创建密钥。如果没有这两个密钥文件,此操作会创建它们,并打印需要填入许可的内容:
provared-mcp-executor keys --config executor.json将打印出的
agent.keys作为 Agent 的密钥填入许可;将打印出的with条目(执行器的 ID、名称和密钥)填入许可的with;并在requires中填写{"need": "countersignature"},这样缺少执行器会签的收据就会被判定为问题。 -
用户使用自己的通行密钥签署许可(可在 https://sign.prova.red 签署页面上完成,或使用库签署),并将其保存到设置指定的位置(
agent.slip)。将通行密钥的指纹填入agent.issuerKeys。 -
检查设置。此操作会读取每条规则、密钥、许可和凭据,并指出问题所在。不会打印凭据。
provared-mcp-executor check --config executor.json不会写入任何内容:不会向账本写入记录,也不会生成副本密钥。
-
将 Agent 的 MCP 客户端指向执行器,即其设置中指定的服务。对于设置采用 JSON 格式的客户端:
{ "mcpServers": { "files": { "command": "provared-mcp-executor", "args": ["stdio", "--config", "/etc/provared-mcp/files/executor.json"] } } }客户端会启动执行器;执行器会使用
server.args,在server.cwd中启动server.command指定的服务,并提供server.env中的凭据。
通过 HTTP 为多个 Agent 提供服务
provared-mcp-executor http --config executor-http.json
执行器会作为 Streamable HTTP MCP 服务进行监听(单个端点;除非设置另有指定,否则为 /mcp;监听地址默认为 127.0.0.1:8796)。它位于一个由 upstream.url 指定的上游 HTTP MCP 服务之前。每个 Agent 都使用自己的密钥登录,在一个请求头 Authorization: Bearer <secret> 中传递;并在 agents.<name> 下拥有自己的许可、密钥、账本和副本。provared-mcp-executor secret 会生成密钥并打印其哈希;将哈希作为 Agent 的 secretHash 写入设置,将密钥交给 Agent,并确保不在其他地方存储。keys 会创建执行器的密钥文件,以及每个 Agent 的密钥文件,并打印各 Agent 许可所需的条目;check 会读取每个 Agent 的许可;compare --agent <name> 会将一个 Agent 的副本与其账本进行比对。
执行器对每个请求所做的处理:
tools/call会遵循与 stdio 相同的规则、检查、回执、共同签署和副本,并记入发送请求的 Agent 的单据和账簿。检查发生在任何内容到达上游之前。每个 Agent 的写入器一次只处理一个调用;各 Agent 之间互不等待。maxWaiting对每个 Agent 分别生效。- Agent 的
Authorization标头会被移除。发送给上游的是执行器自己的凭据,即从文件upstream.credential.file读取的 bearer token。Agent 的标头中,只有Accept、Accept-Language、Content-Type、Mcp-Session-Id和Last-Event-ID会被转发;此外,还会转发请求体所述的MCP-Protocol-Version,以及 2026-07-28 修订版中的镜像标头(前提是其值与请求体一致):Mcp-Method必须与请求体的method一致;对于resources/read,Mcp-Name必须与params.uri一致;对于tools/call或prompts/get,必须与params.name一致;Mcp-Param-<name>必须与调用参数中对应的值一致,该属性须位于上游tools/list标注了x-mcp-header: <name>的工具inputSchema中(比较前会对=?base64?…?=包裹的值进行解码;请求体中的数字会按数字比较,而 JSON 格式的标头数值也视为数字,因此42.0与 42 一致,而0x2A不一致)。所有其他标头都会被丢弃,并记录其名称。若镜像标头与请求体不一致,或执行器无法检查该标头(尚未收到tools/list、没有相应注解、请求为 GET 或响应,或在其他方法中出现Mcp-Name),则会以 400 和 JSON-RPC 错误 -32020(规范中的 HeaderMismatch)拒绝请求,且不会有任何内容到达上游;不含这些标头的请求不会被拒绝。执行器不会自行生成Mcp-Method、Mcp-Name或Mcp-Param-*,但会将请求体所述的协议版本作为标头转发。上游的标头中,只有Content-Type、Mcp-Session-Id、MCP-Protocol-Version和Cache-Control会被传回。 - 每个 POST 只允许一条消息,读取方式与 stdio 行一样严格:批处理、重复的成员名称、非文本或非整数的 ID、大于 2^53 - 1 的数字,或不符合消息结构的内容,都会被拒绝(400),不会继续处理。作为通知发送的
tools/call会被丢弃(202)。 - 上游的答复会原样返回:可以是 JSON 请求体,也可以是逐条转发的事件流。与调用 ID 对应的响应是调用结果,并会结束调用;其他 ID 对应的响应绝不会被视为调用结果。收到结果后,会关闭通往上游的流。若上游以 4xx 状态拒绝调用,并在与调用 ID 对应的响应中返回 JSON-RPC 错误(规范规定的方式:标头不匹配或协议版本不受支持时返回 400,不存在该方法时返回 404),则会将该错误记录为结果,并以其状态码和 Agent 的 ID 转发;若非 2xx 响应没有此类请求体,则会记录为“结果未知”,且不会转发其请求体。
- 旧版会话会透传:上游返回的
Mcp-Session-Id会原样传回,GET 消息流和initialize会透传,会话归打开它的 Agent 所有。其他 Agent 在该会话下发出的请求,或未知 ID(例如重启后执行器不认识的 ID),都会收到 404,客户端随后会开始新会话。 - 取消行为取决于协议时期。在 2026-07-28 修订版中,关闭响应流即表示取消:执行器随后会关闭通往上游的流,并像 stdio 一样将结果记录为未知;若 Agent 针对进行中的调用发送自己的
notifications/cancelled,也会执行相同操作并收到 202,且不会向上游发送任何内容。在旧版会话中(存在会话 ID,或协议版本早于 2026-07-28),规范规定断开连接不表示取消,因此 Agent 的notifications/cancelled会以调用的上游 ID 转发给上游,响应也按上游的答复返回,随后关闭流;仅仅断开 Agent 的连接不会停止调用,调用会继续运行,直至timeout限制到期,并记录其实际结果。 - 请求中若指定了
Origin,除非allowedOrigins列出了该值,否则会被拒绝(403);请求体超过bodyLimit(10 MiB)会被拒绝(413);若请求体未能在timeout内完整到达,则会被拒绝(408)。上游答复或事件超过 16 MiB 时会被截断;若上游未在timeout内答复调用,则会取消调用,并生成一份说明结果未知的回执。若 Agent 的连接未能在timeout内接收执行器等待发送的内容,则该次交互会被中止;此期间上游流会暂停,而不是将内容缓冲起来。读取速度过慢的客户端也可能被中止,因为只有在所有待发送内容都已传输完毕后,计时才会停止。每个 Agent 最多可有 256 个正在处理的请求,并记住 1,000 个会话;达到上限时会优先遗忘最早的会话。JSON 答复即使未被 Agent 读取,在其占用的 256 个请求名额释放后仍会保留;每个连接最多保留 16 MiB,直至该连接关闭。执行器不限制连接数。 - 收到停止请求(SIGINT、SIGTERM)时,若有调用正在进行,会将其结算为执行器停止:“执行器在此调用与工具服务通信期间停止,因此结果未知。”随后写入回执,以该错误答复 Agent,然后关闭。仍在等待轮到自己的调用不会发送,并会收到相同答复。若未收到警告便停止,并随后重新启动,则会在执行器开始监听前,根据 Agent 的副本写入回执,与 stdio 的处理方式相同。
何时向 Agent 隐瞒凭据,何时不隐瞒。 上游的令牌绝不会通过标头、正文、错误或日志传给 Agent:执行器不会转发上游的任何标头,只有指定的四个例外,而且这些标头中的令牌文本也会被替换;不会转发上游拒绝令牌的任何响应(401 或 403;Agent 会被告知执行器的凭据遭到拒绝,仅此而已);绝不跟随重定向;并且会将令牌文本在发送给 Agent 的任何内容中出现的每一处(回显令牌的工具结果、错误文本、事件、标头)替换为 “[a credential was removed by the Provared
executor]”。这适用于令牌在文件中的原始形式;如果上游在回显令牌前对其进行编码或拆分,执行器就无法识别;JSON 对令牌中的
/、"或\自行进行转义,也算作编码。此保护仅适用于经过执行器的内容:能够通过其他方式访问上游或读取执行器文件的 Agent 都能获取令牌。而 Agent 的秘密信息是否安全,取决于其传输连接的安全性:执行器默认监听本地地址并使用纯 HTTP,因此,其他计算机上的 Agent 应通过其他服务提供的加密连接访问它。
设置(可从 executor-http.example.json 开始):name、service、tools、contentRules、unlisted、hideUnlisted、timeout、denialsPerHour、callIdInMeta 和 maxWaiting,如下所述;以及
listen:host(默认127.0.0.1;使用其他地址时会收到警告)和port(默认 8796)。path:端点(默认/mcp)。upstream:url(http 或 https;通过此计算机以外的纯 http 连接时会收到警告)和credential:{"file": <path>},即保存 bearer 令牌的文件,该文件只能由执行器读取。不设置credential时,上游调用不会携带凭据,启动时会说明这一点。agents:为每个 Agent 设置一个成员,以名称命名(1 到 64 个小写字母、数字、句点、连字符或下划线):secretHash、slip、book、keys、issuerKeys,以及copy和copyKey(默认值为copies/<name>.jsonl和copies/<name>.jsonl.key)。任何两个 Agent 都不得共用秘密或文件。executor:keys,执行器自己的密钥文件。bodyLimit:POST 正文的最大字节数(默认 10,485,760)。allowedOrigins:允许其页面调用执行器的来源(协议、主机和端口)。默认不允许任何来源。
设置
{
"name": "Provared MCP executor for the files server",
"service": "executor",
"server": {
"command": "node",
"args": ["files-server.mjs"],
"cwd": "servers/files",
"path": ["/usr/local/bin"],
"env": { "FILES_API_TOKEN": { "file": "secrets/files-token" } },
"passEnv": ["PATH", "HOME"]
},
"agent": {
"slip": "slip.json",
"book": "book.jsonl",
"keys": "keys/agent.jwk",
"issuerKeys": ["<thumbprint of the person's passkey>"]
},
"executor": {
"keys": "keys/executor.jwk",
"copy": "copy.jsonl",
"copyKey": "copy.jsonl.key"
},
"tools": {
"read_file": { "action": "provared.data.read" },
"order_supplies": { "action": "supplies.order", "amount": { "unit": "GBP", "pointer": "/total" } },
"send_messages": { "action": "provared.message.send", "amount": { "unit": "messages", "count": "/recipients" } }
},
"unlisted": "refuse",
"hideUnlisted": true,
"timeout": 300000,
"denialsPerHour": 600,
"callIdInMeta": true,
"shutdownWait": 5000,
"maxWaiting": 16
}
name:执行器的名称;这是其副本每行标签的一部分。service:权限在with中授予执行器的 ID;写入每个存根的with中。默认为executor。server.command:要启动的程序,最好使用其路径指定。裸名称(node)会在server.path的文件夹中查找;如果未提供该项,则会在执行器自身PATH的文件夹中查找,并发出警告:该环境由 Agent 的客户端设置,而客户端随后可能选择将哪个程序提供给凭据。在 Windows 上,裸名称只会尝试添加.exe和.com;服务启动时不会使用 shell,因此无法运行.cmd或.bat垫片(应指定垫片启动的程序,例如node.exe,并将脚本作为参数)。找不到程序时,启动会停止。server.env:服务所需的每个变量,以及其值的读取来源:文件(file;会删除文件末尾的一行换行符)、执行器自身的环境(env,不建议使用:该环境的安全性取决于设置它的 Agent 客户端),或设置本身(value,不建议使用)。服务的环境包含这些变量以及passEnv中的变量(默认为PATH、PATHEXT、SYSTEMROOT、COMSPEC、WINDIR、TEMP、TMP、HOME、USERPROFILE、LANG、LC_ALL、TZ),不包含执行器环境中的任何其他变量:启动执行器的 Agent 客户端会将 Agent 的环境传给执行器,而服务不得继承该环境。agent.issuerKeys:执行器信任的通行密钥的指纹。若凭条由其他任何通行密钥签名,执行器都不会启动。executor.copy、executor.copyKey:执行器自身对每次调用的副本,以及其行所用的标签密钥(32 个随机字节,首次创建时生成)。应将它们和执行器本身置于 Agent 一侧无法写入的用户账户下;只有 Agent 一侧无法更改副本时,副本才会显示账簿中遗漏的收据。tools:Agent 可调用的每个工具,使用其准确名称,并指定凭条为其使用的操作名称,以及适用时的数量:固定的value、调用参数中某个 JSON Pointer(RFC 6901)所指位置的整数(pointer),或该处列表的长度(count)。如果无法读取数量,调用将被拒绝;如果参数在同一对象中两次指定同一个成员,或指定两个仅大小写不同的成员(total和TOTAL),也会被拒绝:服务可能会读取另一个成员。(在每条消息中,成员名称完全重复都会被拒绝;只有规则需要从参数中读取数量时,仅大小写不同的名称才会被拒绝。)contentRules:可选;包含内容规则的文件路径,这些规则会在允许调用通过前检查变更内容(见下文“变更内容规则”)。其指纹会列入每张收据和每项拒绝记录。unlisted:refuse(默认)会拒绝对规则未列出的工具的调用,并写入名为executor.unlisted的拒绝记录;forward会将调用转发出去,不生成收据或拒绝记录,且记录中会遗漏这些调用。使用forward时,如果内容规则指定了tools中没有任何规则列出的工具,启动会停止。hideUnlisted:控制tools/list是否不返回此类工具。默认为 true。timeout:留给服务回复调用的时间,以毫秒为单位(默认为 300,000;范围为 1,000 至 86,400,000)。超时后,执行器会取消调用,并写入带有错误的收据。callIdInMeta:控制是否将执行器为调用生成的标识符通过params._meta["red.prova/call-id"]发送给服务,以便将记录_meta的服务与记录逐行对应。默认为 true。关闭时,存根仅在executor.call下写入该标识符。denialsPerHour:任意一小时内写入的拒绝记录上限(默认为 600)。shutdownWait:关闭输入后,留给服务退出的时间,超时后服务将被终止(默认为 5,000 毫秒)。maxWaiting:同时正在处理或等待轮到的工具调用上限(默认为 16;范围为 1 至 1,000)。超过上限的调用会收到错误 1001,且不会写入任何内容。等待服务回复的tools/list请求最多为 256 个。
变更内容规则
权限允许特定类型的操作。允许更改某个文件,并不意味着允许更改该文件控制的所有内容:更改设置文件中一个标签,并在同一次编辑中启用 scan 设置,这属于一次同类型的调用,而该权限允许这两项操作。执行器可以在自己的 JSON 文件中保存内容规则,该文件由 contentRules 指定,并在放行之前根据这些规则检查每项拟议变更。content-rules.example.json 是一个起点:
{
"version": 1,
"rules": [
{
"name": "the scan setting in settings.json stays off",
"tool": "write_file",
"when": { "pointer": "/path", "matches": "**settings.json" },
"content": { "pointer": "/content", "as": "json" },
"test": { "pointer": "/scan", "equals": false, "orAbsent": true }
},
{
"name": "settings.json is written whole, never edited in part",
"tool": ["edit_file", "apply_patch"],
"when": { "pointer": "/path", "matches": "**settings.json" }
}
]
}
规则包含:
name:1 到 200 个字符的文本,每条规则都必须不同。规则拒绝请求时会向模型显示此名称,并将其写入执行器日志和副本。tool:工具的精确名称,或包含 1 到 64 个名称的列表。when(可选):针对调用参数的一个条件,或最多 16 个条件;所有条件都必须满足,规则才会生效。不设置when时,规则适用于对该工具的每次调用。content(可选):指定拟议内容在参数中的位置(pointer,一个 JSON Pointer,符合 RFC 6901),以及如何读取它(as:json表示参数是包含 JSON 的文本;value表示参数本身就是内容,作为纯数据)。不设置content时,测试直接读取参数本身。只能将其指向服务器会作为完整新内容写入的参数。test(可选):针对内容的一个条件,或最多 64 个条件;所有条件都必须满足。没有test的规则会拒绝所有适用的调用。 当执行器无法判断某条路径时,可以用这种方式将其关闭:例如,某个工具的参数只包含部分编辑内容(旧文本和新文本)、模板,或其他位置内容的引用,导致执行器无法确定最终文件内容。
条件由 pointer 和以下选项之一组成:equals(完整比较,检查是否为相同的 JSON 值)、oneOf(包含 1 到 64 个值的列表)、absent(值为 true,表示该指针处不得有任何内容),或 matches(用于文本的模式:* 匹配除 / 和 \ 以外的任意字符序列,** 匹配任意字符序列,? 匹配一个除 / 和 \ 以外的字符;模式中的 / 或 \ 均可匹配任一分隔符,因此 **/settings.json 可匹配 C:\proj\settings.json;其他字符均无特殊含义)。如果值缺失,equals、oneOf 和 matches 均会判定失败,因为执行器无法知道程序会采用什么默认值;设置 orAbsent: true 可使其通过。如果不读取文件,就无法测试“不得更改”;应改为写明“必须保持与”已批准值相等。
执行器会按文件中的顺序,针对每条指定了该工具的规则测试调用;此操作在读取金额后、执行前检查之前进行。首条拒绝调用的规则会终止调用:模型会收到 Not taken by the Provared executor: Content rule "…": the value at "/scan" in the content at "/content" must equal false, or be absent.,_meta 中会包含 red.prova/refused content-rule 和 red.prova/rule,并记录一次拒绝。如果规则无法准确读取内容,也会拒绝调用(content-unreadable):内容不是 JSON、对象中某个成员出现两次或折叠后出现两次(scan 和 SCAN)、整数超出 9,007,199,254,740,991、内容指针处没有内容、预期为 JSON 文本时却不是文本、需要匹配模式的文本超过 4,096 个字符,或参数中存在此类折叠后重名的名称。如果规则放行了无法读取的内容,那么只要在文件任意位置加上注释就能绕过规则。执行器不会读取任何文件,也不会保留任何内容;它只测试调用携带的内容。参数中的任何值都不会写入模型的错误信息、日志或副本。
记录中的规则文件。 每张回执和每项拒绝都会注明 executor.rules,即规则文件解析内容的 SHA-256 哈希值,采用 RFC 8785 规定的唯一形式(因此,空格不会改变它;任何规则都会改变它);如果拒绝是由规则导致的,还会注明 executor.rule,即规则名称的指纹;两者都是标签。为让读者确认当前生效的规则就是经当事人批准的规则,请将相同指纹写入权限的 agent.software(格式说明第 4.1 节):provared-mcp-executor keys --config executor.json 会在 Agent 的密钥旁打印此条目:{"name": "Content rules of <the executor's name>", "sha256": "<the fingerprint>"}。如果权限声明了软件(agent.software),但其中不包含当前生效的规则文件,Executor 就不会启动:当事人批准的是其他规则。如果权限声明了内容规则(名称以 Content rules of 开头的 agent.software 条目),但设置中没有指定规则文件,Executor 也不会启动:它会在没有当事人批准的规则时运行。如果权限未作声明,Executor 会启动并发出警告。check 会报告读取到的规则以及权限是否声明了这些规则;compare 会针对记录中注明的每个规则指纹,报告注明该指纹的回执和拒绝数量,以及记录簿中的凭条是否声明了该指纹;它还会将 Executor 在声明了内容规则却未指定规则文件的凭条下生成的回执和拒绝列为 withoutRules。
规则文件会经过严格读取校验:UTF-8(不属于任何字符的字节会导致启动停止)、JSON、不得有重复成员名、整数不得超出 9,007,199,254,740,991、"version": 1、不得包含不属于规则文件、规则或条件的成员、JSON Pointer 必须符合 RFC 6901 的写法(~ 只能写作 ~0 或 ~1)、最多 1,000 条规则、文件大小最多 1 MiB,且所有模式片段最多 20,000 个(matches 中的每个字符、? 或连续星号都计为一个),以确保在普通计算机上测试一次调用最多约需四分之一秒;任何不符合要求的情况都会导致启动停止并报告其位置。如果某条规则指定了 tools 中没有列出的工具,并且 unlisted 为 refuse,系统会发出警告:此类调用会在执行任何内容规则之前被作为未列出工具拒绝。如果 unlisted 为 forward,这样的规则会导致启动停止:对该工具的调用会在未测试任何内容规则的情况下被转发。
规则永远无法保证什么。
- 确保无论通过哪种路径,设置都符合规则。针对
write_file的规则无法察觉通过编辑同一文件的 shell 工具、另一个名称不同的工具、规则的when未读取的另一个参数名下给出的同一文件、路径写法与模式预期不同的文件、拆分成两次且每次都能通过的修改,或以另一个名称写入后再移动的文件。任何未通过执行器的方式进行的编辑都完全不会被发现。 - 确保内容是按服务读取的方式读取的。执行器会拒绝无法准确读取的内容,但如果服务以更宽松的方式或采用另一种编码读取相同文本,就可能处理执行器未看到的内容。执行器无法读取的文件类型(YAML、TOML、按行组织的格式;后续工作)无法进行测试:只有不带
test的规则才能拒绝此类写入。 - 确保工具的参数能显示最终文件内容。局部编辑、模板、指向其他位置所存内容的引用或命令,都不会给执行器提供可测试的内容;只有直接拒绝才能阻止操作。
- 确保模式能匹配特定含义。
*和**匹配的是字符,不是文件:**settings.json也会匹配my-settings.json;大小写不同的路径、末尾有空格的路径、末尾有句点的路径(settings.json.,Windows 会将其作为settings.json打开)或符号链接,都属于另一种文本。模式中的/或\会匹配任意一种分隔符,因此一个模式就能覆盖两种写法;在 Linux 或 macOS 上运行的服务中,\是文件名里的普通字符,因此用作允许列表的test(matches: "safe/**")也会接受safe\x,即当前文件夹中的文件。 - 确保
when读取的是正确类型的值。带有matches的when仅在值为文本时才会触发:以列表形式给出的路径(["settings.json"])或任何其他非文本值都不会匹配,因此规则不会生效,调用会继续检查下一条规则。如果服务可能接受其他类型的路径,请使用不带test的规则关闭工具。 - 确保成员名称与程序读取的名称一致。内容中的名称会进行精确比较:
"SCAN": true不等于scan,因此它会通过针对/scan的absent测试或orAbsent测试(同一对象中仅大小写不同的两个成员会因无法读取而被拒绝,但单独的SCAN不会)。如果程序读取名称时不区分大小写,就会处理规则未看到的内容。 - 确保能读取嵌套在内容中的内容。
as: "json"只读取一层:JSON 字符串中的 JSON 文本(作为内容文本的"{\"scan\": true}")对规则来说只是一个字符串,因此针对/scan的测试找不到任何内容。 - 确保规则正确且已应用。记录会保存规则文件的指纹,以及拒绝操作时拒绝该操作的规则指纹;但不会保存规则本身,而回执只能证明写入回执的执行器正在运行这些规则。短规则名称的指纹可以通过一份可能名称列表猜出。
规则编写者必须了解该工具:哪个参数携带完整的新内容、路径在哪个参数名下给出,以及哪些其他工具也能访问同一文件。
执行器如何处理每次调用
- 这条输入行。 严格读取:UTF-8;一个 JSON 对象,且包含
"jsonrpc": "2.0";不得是批量请求;其中任何位置都不得重复出现成员名称; 其中任何位置的整数都不得超过 9,007,199,254,740,991(两个读取方会以不同方式处理它);id 必须是文本或整数。 任何其他内容都会收到 JSON-RPC 错误,不再继续处理:执行器读取的方法必须与服务实际执行的方法一致。(整数规则比网关的规则更严格:它适用于整条输入行,包括参数,而不仅仅是读取金额的位置。)超过 16 MiB 的输入行会被拒绝。方法不是协议自身方法(notifications/…)的通知,例如不带 id 发送的tools/call,会被丢弃并记录日志:它本会未经检查就到达服务;供应商使用其他前缀的自定义扩展通知也会如此处理。每个请求(不只是tools/call)都会使用执行器自己的 id 发送给服务(provared-<number>-<random text>,在会话内唯一且不可猜测),响应则使用客户端的 id 返回给客户端;因此,客户端发送的内容不会与正在处理的调用冲突,而在超时或取消后迟到的响应,也绝不会被误认为是复用同一 id 的后续请求的响应。超过maxWaiting的调用会收到错误 1001。 - 规则,按工具名称执行(名称必须由 1 到 128 个字母、数字、
_、-和.组成;任何其他名称都会被拒绝,并返回 -32602)。 - 金额,按规则的要求处理。然后是内容规则,当设置指定了规则文件时生效:如果规则拒绝某次调用,或规则无法准确读取其内容,则不会发送该调用;模型会获知是哪条规则,并会写入一条注明规则文件和规则的拒绝记录。
- 执行前检查,通过 Agent 的桩写入器进行。若调用被拒绝,则不会发送:客户端会收到一个工具结果,其中
isError: true,文本以 "Not taken by the Provared executor" 开头并说明原因,且_meta包含red.prova/refused、red.prova/codes和red.prova/denial;桩写入器会写入一条带有代码的拒绝记录。 - 副本记录。 一条说明正在发送调用的记录会写入执行器的副本,并确保在任何内容发送给服务之前已写入磁盘。如果无法写入,则不会发送调用(错误 1001),且执行器会停止接收后续调用,直到重新启动。
- 发送。 调用会按客户端编写的内容发送给服务,并在
params._meta中添加执行器的标识符。桩会记录provared.arguments(采用 RFC 8785 的唯一格式表示的参数;没有参数时为{})、provared.tool-name(工具名称)、executor.call和provared.request-id(执行器的标识符);如果调用的_meta携带格式正确的traceparent,还会记录provared.trace-id;如果设置指定了规则文件,还会记录executor.rules(规则文件内容的指纹)。收到响应后,会记录provared.tool-result(服务的整个result,采用 RFC 8785 的唯一格式表示),或者在没有结果时记录provared.error(客户端收到的错误文本,或以 JSON 表示的服务 JSON-RPC 错误)。如果结果无法用该格式写入(文本不是格式正确的 Unicode、数字无法用 JSON 表示,或整数超过 9,007,199,254,740,991),则会记录provared.error,其内容为固定文本,说明原因;执行器无法将响应读取为消息(不是 JSON、成员名称重复)时也会如此处理,但该响应仍会原样到达客户端。重试的多轮往返调用中的inputResponses和requestState不会绑定;只有arguments会绑定。 - 已发送的调用始终会生成桩记录,无论发生什么情况:服务返回结果(包括
isError结果)、返回错误、在timeout内没有响应、Agent 取消调用,或服务结束。结果确定后,执行器会使用自己的密钥为桩记录共同签署,因此共同签名涵盖结果指纹。随后,一条说明回执已记入账本的记录会写入副本,并注明桩记录。如果调用后无法写入账本文件(文件发生变化或不可写),该记录不会注明桩记录,而会记下执行器所知的结果;此时调用仍未完成,执行器会停止接收后续调用,并在下次启动时根据副本中的结果写入回执。 - 返回给客户端的响应是服务返回的原始字节,只将执行器的 id 替换为客户端的 id,不作其他更改:数字保留原有的数字和写法,无论数值多大。(执行器重建的唯一响应是经过筛选的
tools/list,其中每个数字仍按服务原本的写法表示。)如果没有响应,客户端会收到执行器生成的 JSON-RPC 错误:代码 1002(没有响应:已超时、Agent 已取消,或服务已结束),且data中包含桩记录的指纹。若服务返回的输入行不是一个 JSON 对象(例如批量请求或裸值),或其顶层id重复出现,则它不构成消息:会原样转交给客户端,不与任何调用匹配,而该调用会在超时后以“无响应”回执结束。代码 1001 表示调用未被接收:执行器无法记录、已停止,或等待中的调用过多。执行器因其他原因无法将输入行读取为消息(非 UTF-8、非 JSON),且无法找到其顶层id时,也按此处理;超过 16 MiB 的服务输入行会被丢弃。若服务始终未响应,则非工具调用请求在此类请求累计达到 1,000 个后会被遗忘,最早的请求优先;如果其响应之后到达,会以执行器的 id 转交,客户端不知道该 id,因此会忽略该响应。 一次工具调用会依次通过存根写入器;其他消息会立即通过。耗时较长的工具调用会延迟下一次工具调用。
取消。 如果客户端针对正在与服务端交互的调用发送 notifications/cancelled,该通知会被传递;随后将结果视为未知(规范规定服务端不会再为该调用发送任何内容),回执会注明 provared.error,迟到的答复会被丢弃。仍在等待轮次的调用不会发送任何内容,也不会写入任何内容;副本会将其标记为已撤回。
服务端结束。 与该服务端交互的调用会收到注明 provared.error 的回执;仍在等待的调用会收到错误 1002,且不会留下记录;执行器会以服务端的退出代码退出,客户端会按规范要求重启它。
执行器停止时正在发送的调用。 其在副本中的那一行没有注明回执的行。下次启动时,会根据该行写入回执,并遵循当时生效的权限:如果副本中保存了结果或错误的指纹,则写入带有该指纹的回执(回执已生成,但无法写入账簿);否则注明 provared.error,并附上文字“The executor stopped while this call was with the tool server, so its outcome is not known…”。此类回执还会注明文档 executor.written-late(固定文本的指纹),因此账簿本身会说明这是执行器在重启时写入的:日期为调用发送的时间,副署日期为重启时的时间,读者可以看到这两个时间不同。如果调用是在其他权限下发送的,执行器不会启动,并会提示将该权限恢复一次以便启动。
副本无法读取(副本中有行但缺少密钥;没有行能通过密钥校验;最后一行因停止而被截断;某行被删除、重复插入或移动,这些情况可由每行携带的编号看出):执行器不会启动,并会说明原因。副本末尾被截断不会被发现:没有任何行会标明自己是最后一行。
副本,以及与账簿的比较
provared-mcp-executor compare --config executor.json [--book <another copy of the book>]
provared-mcp-executor compare --config executor-http.json --agent <name> [--book <another copy of the book>]
对于副本中显示为已发送的每次调用,都会在账簿中查找注明该执行器和相同 executor.call 的存根。答复会列出匹配的调用、账簿中的回执与执行器写入的回执不同的调用(differs)、账簿中没有回执的调用(missing:账簿维护者遗漏了该回执,或账簿比副本旧)、执行器自身无法写入账簿的回执(notWritten:可与遗漏区分的执行器自身停止)、执行器已发送但从未给出回执的调用、账簿中注明该执行器但副本不认识的回执,以及账簿缺少的拒绝记录。此外,withoutRules 会列出执行器在一份声明了内容规则但未注明规则文件的凭条下生成的回执和拒绝记录;这不会更改代码。只有在所有内容一致、副本的每一行都能通过密钥校验且按顺序运行,并且账簿本身没有问题时,才会以代码 0 结束。副本密钥必须存在(compare 不会创建密钥),通过 --book 指定的账簿也必须存在。
它无法找到从未通过执行器的调用,无法判断某个工具是否按结果所述执行了操作,也无法发现由持有副本密钥的人修改的副本。
它不做什么,以及它不能证明什么
- 未经过它的调用。 它无法证明任何事情。不要把凭据交给 Agent。
- 工具的实际行为。 操作是规则根据工具名称判定的;参数和结果只会生成指纹,不会被理解。工具实际执行的操作如果超出名称所示范围,记录时仍会归入规则判定的操作。
- 重试。 每次
tools/call都会生成一张回执,包括要求提供更多输入的调用的每次重试(resultType: "input_required");每次调用都会计入总数和计数。 - 时间。 时间取自执行器的时钟。目前还没有写入密封记录或外部时间戳。
- 单个服务。 一个执行器位于一个工具服务前面:对于单个 Agent,使用 stdio;对于多个 Agent,使用 HTTP;每个 Agent 都有自己的凭条和账簿。密封记录、
acknowledge命令、将副本发送到其他位置,以及将密钥存放在密钥服务中,这些都已在DESIGN.md中设计,但尚未实现。 - 通过 HTTP 时,Agent 的密钥会以明文传输,除非有其他机制加密连接;执行器默认监听本地地址。上游提供的会话 ID 只会在执行器运行期间绑定到开启该会话的 Agent。
- 测试情况。 通过 HTTP 测试时,执行器只与官方 MCP 软件在协议版本 2025-11-25 下进行了测试,且所有程序都在同一台计算机上;截至 2026-07-28,它只连接过自己的测试服务(
test/fake-http-server.mjs)。 - 内容规则只检查调用携带的内容,无法证明任何未经过执行器的调用;参见上文“规则永远无法保证什么”。
- 副本的安全性取决于其所在文件夹;任何持有副本密钥的人都可以编写通过校验的记录行。
在仓库副本中进行开发
cd mcp-executor
npm test
测试会将一个虚构的 MCP 服务(test/fake-server.mjs)作为真实进程启动,并通过其流与执行器通信;还会在本地地址启动一个虚构的 HTTP MCP 服务(test/fake-http-server.mjs),并在它前面测试 HTTP 执行器;所有内容都不会离开这台计算机。provared 指的是仓库自带的库(test/local-library.mjs),因此测试使用的是同一提交中的该库。
demo/ 中包含一个演示:执行器位于一个虚构的工具服务前面,并在首次启动时使用该库的开发用通行密钥替代方案签署的权限进行配置,因此无需设置任何参数或密钥即可启动。其记录不能证明任何人的行为;参见 demo/README.md。server.json 描述了要提交到官方 MCP Registry 的软件包,其中版本 0.2.0 列为 io.github.provared/mcp-executor;它不属于 npm 软件包。只有在同一版本已发布到 npm 后,才能将该版本发布到 Registry,因为 Registry 会从 npm 读取该版本的 package.json,并将其中的 mcpName 与 server.json 中的名称进行比较。要在 io.github.provared/ 名下发布,必须由 GitHub 组织 provared 的 Owner 登录,并使用能够读取组织成员信息的个人访问令牌(带有 read:org 权限的经典令牌,或具有 Organization 权限、Members 权限且为 Read-only 的细粒度令牌),也可以通过 GitHub Actions 发布;目前,不带令牌的 mcp-publisher login github 无法向组织名下发布。
许可证
代码采用 Apache License 2.0(LICENSE);文档采用 Creative Commons Attribution 4.0(LICENSE-CC-BY-4.0.txt)。版权所有 © 2026 Pavel Izmaylov。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。