MCP server
Model Context Protocol server 提供 Zuno 自身不附带的工具与资源。一个 server 要么作为子进程在本地启动,要么通过 HTTP 触达。无论哪种方式,它的工具都进入与原生工具相同的权限与授权路径;不存在第二套仅供插件使用的权限语言。
在注册任何东西之前,有一个默认值值得先知道:未知的 MCP 工具被视为有副作用。它失败即拒绝,而不是被假定安全。
注册一个 server
zuno mcp add my-server -- npx -y @modelcontextprotocol/server-filesystem /srv/data
zuno mcp add remote-server --url https://mcp.example.com --header "Authorization: Bearer $MCP_TOKEN"
zuno mcp list2
3
本地 server 的命令写在 -- 之后。--env 传递环境变量,--header 传递 HTTP 头。
或者直接配置
mcp 映射有三种形态:本地 server、远端 server,或者对另一配置层已定义的 server 的开关。
{
"mcp": {
"filesystem": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/srv/data"],
"environment": { "LOG_LEVEL": "warn" },
"cwd": "/srv/data",
"enabled": true,
"timeout": 5000
},
"docs": {
"type": "remote",
"url": "https://mcp.example.com",
"headers": { "X-Tenant": "tenant-a" },
"timeout": 5000
},
"inherited-server": {
"enabled": false
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 字段 | 适用于 | 含义 |
|---|---|---|
type | 两者 | local 或 remote |
command | local | 命令与参数,必填 |
cwd | local | 工作目录;相对路径从工作区解析 |
environment | local | server 进程的环境变量 |
url | remote | server URL,必填 |
headers | remote | 每次请求都发送的头 |
oauth | remote | OAuth 设置,或用 false 抑制自动探测 |
enabled | 两者 | 是否启动该 server |
timeout | 两者 | 请求超时(毫秒);运行时默认值是 5000 |
开关形态只接受 enabled,项目层就是这样在不重复定义的前提下禁用一个全局定义的 server。
本地 server 的 command 是一个本机可执行文件,声明它的那一层很重要。写在项目 zuno.json[c] 或 .zuno 文件里会被拒绝:配置发现以校验错误失败,并指明 mcp.<name>.command,因为那等于让 被检出的仓库替你选择一个以你的权限在本机运行的程序。命令旁边写 "enabled": false 也一样:这个 开关与命令同在检出可控的那一层,之后任何一层都能在不重述命令的情况下把它打开。远程 server 不在 本机运行任何东西,永远不会被拒绝;开关形态则是项目层关闭一个全局定义 server 的方式。
要让某个检出保留自己的本地 server,在受信层里点名它:
{
"trust": {
"project_host_commands": ["/home/you/src"]
}
}2
3
4
5
true 信任本机上的每个检出,false 是默认值,列表则信任这些绝对路径根目录之内的项目配置文件。 项目层自己设置 trust 会被拒绝,因此授权始终来自全局 zuno.json、ZUNO_CONFIG、 ZUNO_CONFIG_CONTENT、受管配置目录或 macOS 受管偏好。被接纳的声明仍会逐个键记入日志。完整的 优先级规则见配置文件与优先级。
认证
对于需要 OAuth 的远端 server:
zuno mcp auth remote-server
zuno mcp auth list
zuno mcp logout remote-server2
3
OAuth 块控制整个流程:
{
"mcp": {
"docs": {
"type": "remote",
"url": "https://mcp.example.com",
"oauth": {
"clientId": "zuno-local",
"scope": "read:docs",
"callbackPort": 19876
}
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
| 字段 | 含义 |
|---|---|
clientId | 客户端 id;缺省表示使用动态客户端注册(RFC 7591) |
clientSecret | 客户端密钥,当授权服务器要求时使用 |
scope | 要请求的 scope |
callbackPort | 本地回调端口;运行时默认值是 19876 |
redirectUri | 完整的 redirect URI,会覆盖 callbackPort |
对于一个公布了 OAuth 元数据但你不希望使用的 server,设置 "oauth": false 来抑制自动探测。对于静态 token,用一个头比走 OAuth 流程更简单,但请把值放在环境变量里,而不是 JSON 文件里。
OAuth 元数据文档按 1 MiB 上限读取,而一个超限文档意味着什么,取决于是谁选定了它的地址。如果 URL 是 server 自己在 WWW-Authenticate 挑战里给出的,超限会让登录以 OAuth 错误失败,因为地址和大小都是它自己选的。Zuno 从配置 URL 推导出来的 .well-known 路径只是猜测,所以那里的超限响应只记一条告警并继续尝试下一个候选 —— /.well-known/oauth-protected-resource 上的一个兜底页面不会让你无法登录。两条路径上超限的文档都绝不会被解析;而如果整个发现过程最终没拿到任何授权服务器元数据,它仍然会失败,而不是去猜。
ACP session server
ACP 客户端可以在 new/load/resume 时提供完整的 session-local mcpServers 列表。这里支持 stdio 与 Streamable HTTP,不支持 SSE。这些 server 不会写进 zuno.json:它们是经过校验、只存在于进程内的 effect,全部连接并完成 discovery 后工具才会发布,部分启动按逆序清理。客户端 command、environment 值与 header 永不写入 session 数据库或日志。参见 zuno acp。
MCP 工具如何变为可用
注册不等于授权。要让模型调用一个 MCP 工具,四件事必须同时成立:
- 该 server 已启用且连接成功,或已有通过连接身份校验的工具目录,由会话代理在首次调用时连接成功。
- 在配置了 Agent 确切
tools允许列表的情况下,该工具的 wire id 在其中存活。 - 没有显式权限规则拒绝它。
- 对于被委派的回合,那个确切的 schema 在父级 Attempt 中可见。
由于未知的 MCP 操作默认为有副作用,授予一个经过审计的查询工具,并不会让某个 Agent 一并获得该 server 暴露的所有工具:
{
"permission": {
"mode": "standard",
"rules": {
"docs_search": "allow",
"docs_write": "deny"
}
}
}2
3
4
5
6
7
8
9
MCP 与扩展工具不会自动对每个只读 Agent 可用。具备工作能力的角色可以选择自动继承,只读 Agent 也可以在自己的规则中被显式授予一个经过审计的工具 id,但父级 schema 上限与确切 schema 校验仍然生效。参见 Agent。
直接可见与渐进式 schema 发现
通过上述四道门禁意味着 MCP 工具可执行,并不意味着 Zuno 必须在每次 provider 请求中 注入所有已连接 schema。默认 auto 把小型服务的整个工具集放入首个请求:每个服务不超过 8 个工具,共享自动预算为 32 个工具、65,536 字节的 schema/描述。较大工具集才延迟展开; tool_search 显示原始服务名和有界能力摘要。Agent 会主动按任务选择相关 MCP,不需要用户 先点名 server。
tool_search 搜索工具 id、服务名、显示名称和描述;匹配项会累积加入 下一次 provider step,不能在发现它们的同一批 assistant 工具调用中立即调用。
这只改变提示词暴露,不改变授权:搜索无法恢复被权限拒绝或被能力过滤移除的工具。Agent 确切 tools 允许列表被视为有意选择 schema,其中点名的 MCP 工具保持立即可见。被委派 的子级只接收父级 Attempt 已记录的确切 schema,不能通过搜索获得更大的目录。
工具可见性与连接配置分离,调整下列策略不改变连接身份、不丢弃已校验目录缓存,也不建立 全局 MCP 进程池:
{
"mcp_tool_exposure": {
"mode": "auto",
"auto_tool_limit": 32,
"auto_schema_bytes": 65536,
"small_server_tool_limit": 8,
"servers": {
"aws-knowledge-mcp-server": "eager",
"figma": "deferred"
}
}
}2
3
4
5
6
7
8
9
10
11
12
eager 使已授权的工具定义首轮直接可见,deferred 经发现入口展开;服务设置覆盖全局 mode。 会话/Agent 精确指定的工具仍立即可见,不受自动预算限制;强制全部 eager 会增加上下文开销。 可见性不等于授权,不能放行原本被拒绝的工具。
工作 Agent 的默认规则明确允许发现入口,用户覆盖仍最后生效。若 tool_search 被禁用或 拒绝,其他已获授权的 MCP schema 保持直接可见,不会被困在不可达的加载器之后; MCP 工具本身的显式拒绝仍有效。
延迟展开不等于断开连接,enabled: true 也不等于已连接。普通任务直接使用可见 MCP 工具, 大型延迟工具才用 tool_search。资源列表、扩展列表及 customize-zuno 不是使用 MCP 的前置 步骤;不应通过 Shell 手写 MCP HTTP 绕开发现。明确要求开发/调试传输时另当别论。
ACP session-local mcpServers 同样属于客户端显式契约。严格连接门禁成功后,它们的 schema 会出现在第一次 provider 请求中;同一会话里的宿主配置 MCP server 采用上述可见性 策略。该区分会随会话传递到子回合与后台续跑。
provider 请求快照记录搜索后的确切工具 schema;搜索结果也会把匹配 id 与单调递增的 目录 revision 写入持久工具结果。后台唤醒、进程重启或客户端重新挂载同一会话时,这些成功 结果会恢复此前暴露的子集;恢复仍与当前连接目录和有效权限取交集,因此不会复活已移除能力。
后续每个工具调用都会保存准入它的 provider-visible schema identity。组装更早 turn 的保留 历史时,缺失或已变化的声明不会被静默绑定到新 schema,也不会继续作为无声明的历史函数调用 发给 provider;它只在本次请求中变成惰性 JSON 转录块。当前 turn 刚产生的调用仍保持原生 配对,因此未知或被拒绝的调用可以回送协议完整的 tool result。Zuno 发出 warning,持久原记录 保持不变。如果另一个已注册工具已经定义了 tool_search,Zuno 不会遮蔽它:该回合保持 schema 立即可见,并由宿主发出警告。
TUI 在下一个回合边界接纳目录变化,不在工具执行中途替换传输或工具快照。 debug agent 的 schemaExposure 展示 eager/deferred id、策略、来源元数据与发现入口状态。 它使用独立诊断 runtime;当前 TUI 的实际连接状态请看 /mcp。
并发与超时
{
"concurrency": {
"mcp_connections": 8
}
}2
3
4
5
mcp_connections 限定跨不同 server 的同时生命周期操作数。同一个 server 的操作保持串行,这避免了第二次连接与它自己的断开竞速。该字段接受 1..=64。
请求超时按 server 设置:在条目上写 timeout。不存在全局的 MCP 超时键。
MCP 调用失败按「失败在哪」分类,而不是只看「失败了」。5xx、408、429 响应以及连接被断开属于可恢复失败,回合会带退避重试。其余 4xx —— 400、401、403、404、405 —— 以及 OAuth 失败(例如你设了 "oauth": false 的 server)则直接阻塞,因为在配置改变之前,同样的请求只会同样地失败。
客户端自己的超时被当作第三类,因为请求可能已经在 server 上生效了。远端工具代理声明自己不可重放,所以一次超时的调用会带回一条恢复提示:副作用可能已经发生,在权威外部状态证明它没有完成之前不得重放。两个资源列举工具声明自己可以安全重放,因为 resources/list 不改变任何状态。
读不出来的响应属于同一类第三种情况。如果一次可能改变 server 状态的调用返回了 Zuno 无法解析的响应体 —— 被代理截断、SSE 事件里是非法 JSON,或者流在回复到达之前就结束了 —— 写入其实已经到达 server,所以这次调用报告的是「结果未知」,而不是一次确定的失败,并且不可重放策略照旧生效。像 tools/list、resources/read 这类只读方法则直接报告协议错误,因为 server 上不可能发生任何改变。
stdio 分帧有上限。单个 JSON-RPC 帧最大 64 MiB,是一个 server 合法能发出的最大 base64 资源块的四倍以上。超长帧会让所有待响应的调用以协议错误失败并关闭该流,因为在帧中间被截断的流无法重新同步。server 的 stderr 按每行 8 KiB 截断,而不是当作致命错误,因为它没有待响应的调用方。
不是 JSON-RPC 的 stdout 会被诊断,而不是干等到超时。凡是不能解析成 JSON-RPC 消息的行 —— 包括空行 —— 都按连接计数;一个 server 如果连续产生 32 行这样的输出却始终没有成功分帧出一条可解码的消息,它的待响应调用会以协议错误失败并说明这个计数,而不是一直等到超时却什么都报不出来;日志按每 1024 行记录一条告警,而不是每行一条。一旦某个 server 成功分帧过一条消息,后续的噪声就再也不会导致断开。base64 资源块与 image 块在去掉换行之后再度量,Zuno 存下的附件正好就是被度量的那些字节,所以给载荷加填充无法放宽 10 MiB 的解码后上限。
工具没出现时
zuno mcp list
zuno mcp debug my-server
zuno debug agent build
zuno debug permissions2
3
4
mcp debug 探测一个已注册的 server,从而把 server 故障与注册故障区分开。debug agent 显示该工具是否在 Agent 的能力过滤中存活,debug permissions 显示是否有规则拒绝了它。在怀疑 server 之前先检查这两项最省时间,因为被允许列表隐藏的工具,看起来和一个启动失败的 server 一模一样。
MCP prompt
server 还可以暴露 prompt,它们会成为命令。Zuno 向 server 请求它的 prompt,并把每个声明的参数绑定为字面量 "$1"、"$2" 等,然后用你真实的参数填充这些占位符。参见 Workflow 与命令。