MCP
ZCode 的 MCP 客户端:配置从用户、项目、插件与内置宿主四处合并,支持 stdio、http、sse 三种传输;会话一创建就开始连接,首次模型请求前注册工具;工具命名与图片归一、OAuth 两段式授权、官方 MCP 的身份头注入,以及工作区级 MCP 自动连接的安全含义。
ZCode 只实现了 MCP 的客户端一侧,而且只用它的一项能力:把外部服务器暴露的工具接进 Agent 的工具表。端口 McpPort 只有连接、断开、探活、查状态、列工具、调工具、关闭这几个方法(apps/zcode-cli/packages/contracts/src/interfaces/mcp.port.ts:290),资源(resources)与提示词(prompts)都不在其中;客户端构造时只传了版本协商选项,没有声明 roots、sampling 之类的客户端能力(apps/zcode-cli/packages/adapters/src/mcp/index.ts:1042),代码里也没有处理 tools/list_changed 通知的地方,工具表在连接时一次定型。
代码分四层。apps/zcode-cli/packages/adapters/src/mcp 的 19 个文件约 6000 行是适配器,基于官方 TypeScript SDK @modelcontextprotocol/client 2.0.0(apps/zcode-cli/packages/adapters/package.json:105),管传输、连接、进程回收、OAuth 与官方鉴权;apps/zcode-cli/packages/core/src/mcp 把工具描述投影成运行时的工具条目,并归一结果里的图片;apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts 决定何时连接、何时注册;bootstrap 把各路配置合并成一张服务器表。端口类型经 @zcode/contracts/mcp 子路径导出,指向的就是上面那个 mcp.port.ts。浏览器控制所用的 node_repl 也是一台 MCP 服务器,留给下一篇。
怎么用
配置写在主配置文件里,用户级默认是 ~/.zcode/cli/config.json(apps/zcode-cli/packages/adapters/src/config/file-config.adapter.ts:61),服务器放在 mcp.servers 下;features.mcp 缺省为真(apps/zcode-cli/packages/adapters/src/config/index.ts:289)。README 的示例(apps/zcode-cli/README.md:144):
{
"features": {
"mcp": true
},
"mcp": {
"servers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"cwd": ".",
"timeoutMs": 30000
},
"docs": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
},
"legacy-sse": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"enabled": false
}
}
}
}字段以 schema 为准(apps/zcode-cli/packages/adapters/src/config/schema.ts:46):
| 传输 | 必填 | 可选 |
|---|---|---|
stdio | command | args、cwd、env |
http | url | headers、oauth |
sse | url | headers、oauth |
| 三者共有 | —— | enabled、timeoutMs、protocolVersion(auto、legacy、2026-07-28) |
README 的字段说明(apps/zcode-cli/README.md:176)漏了 oauth 与 protocolVersion。解析前还有一层宽容的归一(schema.ts:331):不写 type 时有 command 就当 stdio、有 url 就当 http,type: "remote" 改成 http;旧字段 environment、enable、http_headers 分别映射到 env、enabled、headers,别家配置里的 timeout、startup_timeout_sec 直接丢弃。单台服务器校验不过只跳过它并记一条诊断,不连累整份配置(schema.ts:488)。
在 CLI 里查看和管理用 /mcp(apps/zcode-cli/packages/cli/src/command-center/handlers/mcp.ts:5):
| 命令 | 作用 |
|---|---|
/mcp、/mcp list、/mcp status | 三者相同,逐台列出状态、传输、工具数与错误 |
/mcp connect <server> | 按已配置的条目重新连接,未配置的名字直接报错 |
/mcp disconnect <server> | 断开当前连接 |
TUI 侧栏的 MCP 分区每 5 秒刷新一次状态,出错时改为 10 秒(apps/zcode-cli/packages/tui/src/app-mcp-status.ts:5),最多列出 5 台(apps/zcode-cli/packages/tui/src/app-sidebar-mcp.tsx:16)。
配置从哪来
项目配置的发现规则与 Hook 共用:从工作目录向上找到含 .git 的目录为止,沿途每一级的 zcode.json 和 .zcode/config.json 都算(packages/shared/src/workspace-hook-config.ts:174、workspace-hook-config.ts:335),找不到 .git 就只看工作目录本身。全局配置按系统默认、用户、项目、环境变量、命令行的顺序覆盖(apps/zcode-cli/packages/adapters/src/config/config-factory.ts:122),MCP 服务器却单列一条规则:同名时用户配置压过项目配置(config-factory.ts:391)。环境变量层没有任何 MCP 相关的变量(apps/zcode-cli/packages/adapters/src/config/env-config.adapter.ts:14),CLI 也没有对应参数,所以文件层实际只有用户与项目两级。README 说当前 CLI 不会自动发现插件以外的 mcp.json、.mcp.json(apps/zcode-cli/README.md:141),这没错,但它没提项目目录里的 zcode.json 与 .zcode/config.json 同样会被读进来。
文件层之外还有三路,在 bootstrap 里合并(apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:90):
// Protocol session/create 传入的 mcp.servers 只包含 UI MCP 设置里的用户配置,
// 不包含插件注册的 MCP。宿主内建 server 最后合并并保留其 identity,避免用户或第三方
// 用同名配置劫持 mcp__node_repl__*;普通 plugin MCP 仍允许显式用户配置覆盖。
const configuredMcpServers = {
...pluginMcpServers,
...(options.runtimeConfig?.mcp?.servers ?? configResult.config.mcp.servers),
...builtInMcpServers,
};
// ...
// 产品决定 workspace MCP 开箱即用:project 作用域 MCP 默认 trusted,并自动连接。
const untrustedProjectMcpServers = new Set<string>();
const autoConnectMcpServers = omitMcpServers(
configuredMcpServers,
untrustedProjectMcpServers,
cuaBridgeServerNames,
);- 插件:服务器键被加上
plugin:<插件名>:前缀(apps/zcode-cli/packages/adapters/src/plugins/mcp.ts:67),处在最底层,用户可以用同名条目覆盖。清单写法与变量展开见插件与官方市场。 - 桌面端:经 ZCode Protocol 创建会话时带上设置页的服务器表,有它就整体替换文件配置(
runtime-config.ts:95)。这张表由桌面主进程读~/.zcode/cli/config.json、工作区.zcode/config.json,并把.agents/mcp.json的mcpServers当兜底来源(packages/desktop/src/main/mcpUserDirectory/index.ts:37、mcpUserDirectory/index.ts:52);桌面端还会不落盘地把当前工作区路径追加到@modelcontextprotocol/server-filesystem的参数里(packages/services/src/session/mcpWorkspaceScope.ts:46),设置页也能把本机条目同步到远程工作区(packages/services/src/mcp-sync/mcpSync.ts:18);界面一侧的 MCP 类型另在packages/shared/src/mcp.ts:24。 - 内置:
node_repl最后合并,同名的用户或插件条目劫持不了mcp__node_repl__*。
omitMcpServers 的第二个参数本是“未受信任的项目服务器”名单,这里恒为空集,后文再谈它的含义。
三种传输与超时
createTransport 按 type 直接选传输,不存在失败后换一种再试的回退(apps/zcode-cli/packages/adapters/src/mcp/index.ts:1407):
| 传输 | 实现 | 要点 |
|---|---|---|
stdio | ProcessTreeStdioClientTransport,继承 SDK 的 StdioClientTransport | cwd 相对工作目录解析,缺省即工作目录;stderr 走管道,末尾 4000 字符脱敏后才进失败日志(adapters/src/mcp/index.ts:97、adapters/src/mcp/index.ts:1901) |
http | SDK 的 StreamableHTTPClientTransport | headers 作静态请求头,fetch 遵循 ZCode 的代理与 CA 设置 |
sse | SDK 的 SSEClientTransport | 协议时代强制 legacy |
README 说 stdio 进程继承 zcode 的环境再叠加 env,实际并非原样继承:先剔除一批 ZCode 私有的运行时变量(代理、证书、CUA 凭据等),再按网络设置重新写入代理与 CA 变量,最后才叠加配置里的 env(apps/zcode-cli/packages/adapters/src/mcp/network.ts:9);如果当前 Agent 由 node 可执行文件启动,还会把它所在目录补进 PATH,让插件里写的 command: "node" 在远程主机上也能起来(network.ts:78)。清洗规则见执行边界。
SDK 2.0 区分 modern 与 legacy 两个协议时代,由 protocolVersion 决定协商方式:2026-07-28 钉死 modern,legacy 或 sse 传输走 legacy,其余一律 auto(apps/zcode-cli/packages/adapters/src/mcp/index.ts:1773)。auto 先发一次探测,预算是连接超时的一半、封顶 5 秒;钉死版本时没有回退,探测就是唯一的握手,所以用满整个连接超时(adapters/src/mcp/index.ts:1815)。协商失败单独归为 protocol_negotiation_failed,不会再被误报成“网络不可达”(adapters/src/mcp/index.ts:1232)。各项超时:
| 项 | 数值 | 出处 |
|---|---|---|
| 连接握手、列工具 | 各 30000 毫秒,可被 timeoutMs 覆盖 | adapters/src/mcp/index.ts:93、adapters/src/mcp/index.ts:1057、adapters/src/mcp/index.ts:1067 |
| 工具调用 | 30000 毫秒,收到进度通知时重新计时 | apps/zcode-cli/packages/core/src/mcp/index.ts:37、apps/zcode-cli/packages/adapters/src/mcp/index.ts:703 |
| 存活探测 ping | 5000 毫秒与 timeoutMs 取小 | adapters/src/mcp/index.ts:95、adapters/src/mcp/index.ts:401 |
| 会话启动时等 OAuth | 15000 毫秒 | apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:11 |
| OAuth 授权事务 | 300000 毫秒 | apps/zcode-cli/packages/adapters/src/mcp/oauth-interactive.ts:38 |
| 连接池空闲回收 | 30000 毫秒 | apps/zcode-cli/packages/adapters/src/mcp/pool.ts:16 |
连接生命周期
何时连接。AgentRuntime 构造的最后一步就调用 startMcpStartup(apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:307),在后台对整张服务器表发起连接,并把等待 OAuth 的时间压到 15 秒;注释说以前没人授权时会等满 5 分钟,模型请求迟迟发不出去(apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:73)。回合循环在组装工具表之前等它完成并注册工具(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:106),这就是 README 所说的“在第一次模型请求前注册”(apps/zcode-cli/README.md:180);手动压缩与插件引用提醒也会先等它(apps/zcode-cli/packages/core/src/runtime/methods/compact-active.ts:253)。注册只做一次,initializeMcp 靠 mcpToolsRegistered 标记保证幂等(methods/mcp.ts:126)。连接开始得比多数人以为的更早:TUI 主界面挂载后(不需要登录时)立即开始轮询 MCP 状态(apps/zcode-cli/packages/tui/src/app-view.tsx:110),轮询经 getApp() 把会话建出来(apps/zcode-cli/packages/cli/src/tui-prompt-handler-queries.ts:61),所以打开 TUI、还没输入第一句话,配置里的 stdio 命令就已经跑起来了。
逐台连接。connectConfiguredServers 是“替换”语义:先断开不在新表里的服务器,再并行连接其余各台(adapters/src/mcp/index.ts:253)。单台的 openServerConnection 依次建传输、构造 Client、带超时 connect、带超时列工具,把工具描述规范化后存进记录,最后挂上 onclose(adapters/src/mcp/index.ts:1002)。
失败。任何一步出错都进 failConnection:关掉客户端与传输,记录置为 failed、清空工具,并附一个 failureKind(adapters/src/mcp/index.ts:1327)。分类共 19 种(packages/shared/src/zcode-protocol/index.ts:663),常见的是 process_start_failed、network_unreachable、connection_timeout、protocol_negotiation_failed、tool_list_failed、oauth_authorization_failed。整次启动的异常也被吞掉、退化成空快照(apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:100),MCP 出问题不会挡住回合,只是少了工具。
断连与重连。onclose 把意外断连记成 disconnected,分类 unexpected_disconnect,并把 stdio 子进程的退出码与 stderr 尾部写进日志(apps/zcode-cli/packages/adapters/src/mcp/index.ts:1119)。恢复发生在下一次调用(adapters/src/mcp/index.ts:455):
// stdio MCP 子进程死亡后(如 node_repl 被异步错误击穿),此前没有任何恢复路径:
// 连接只在 session 创建时建立一次,session resume 也不重建,该会话的工具从此永远失败。
// 这里在调用前对已断连的 record 重连一次;server 进程内状态(如 REPL 变量)不可恢复,
// 但工具本身恢复可用。
const disconnected = this.records.get(request.serverName);
if (disconnected && disconnected.status.status === "disconnected") {
await waitWithinMcpDeadline(
this.reconnectForCall(request.serverName, disconnected.config),
deadline,
timeoutMessage,
options.signal,
);
}SDK 可能在 onclose 派发之前先抛出裸的 Not connected,对这一种错误也重连后重试一次(adapters/src/mcp/index.ts:497)。HTTP/SSE 服务停掉时没有常驻流可断,状态会一直停在 connected,所以桌面端设置页刷新时带上 revalidate,先 ping 一下再决定是否重连(apps/zcode-cli/packages/adapters/src/mcp/pool.ts:111)。
关闭。主动关闭先摘掉 onclose,再按进程树回收 stdio 服务器,最后才调 SDK 的 close,因为 SDK 只保证直接子进程退出,npx 之类包装器拉起的后代会残留(adapters/src/mcp/index.ts:1625)。POSIX 上依次发 SIGINT、等 250 毫秒、SIGTERM、等 750 毫秒、SIGKILL(apps/zcode-cli/packages/adapters/src/mcp/process-tree.ts:196);Windows 上启动时把子进程放进一个“句柄关闭即终止”的 Job Object(apps/zcode-cli/packages/adapters/src/mcp/windows-job-object.ts:4),回收时再用 taskkill /T /F 兜底,超时 2 秒(process-tree.ts:173)。独立 CLI 的会话自己拥有适配器,关会话时它与执行端口、浏览器会话并行关闭,各给 6 秒(apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:301、session-facade.ts:82)。
桌面端的连接池。app-server 进程里是一个连接池,每个会话领一份租约(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-entrypoint.ts:271)。连接默认按会话隔离,只有声明了 isolation: "workspace" 的服务器才在同一工作区的多个会话间共享(pool.ts:447),租约全部释放后再等 30 秒才真正关闭。isolation 只由宿主生成,用户配置的 schema 不接受它。桌面端还用 McpTelemetryTracker 跟踪每个 stdio 子进程的启动、崩溃与资源占用(apps/zcode-cli/packages/adapters/src/mcp/telemetry.ts:41),相关上报见遥测、调试与提示词轨迹。
两个命令的边界。/mcp connect 只在端口层重连(session-facade.ts:361),工具注册却只在会话里做一次(apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:138):从代码看,启动时就失败的服务器事后连上,它的工具不会补进当前会话,要换新会话才出现。/mcp disconnect 也不是封禁,记录变成 disconnected 之后,模型下一次调用它的工具就会触发上面的自动重连。
工具命名与模型契约
模型看到的名字在适配器里生成(apps/zcode-cli/packages/adapters/src/mcp/descriptor.ts:14):
const toolName = typeof record.name === "string" ? record.name : "unknown";
return {
serverName,
toolName,
name: `mcp__${sanitizeMcpName(serverName)}__${sanitizeMcpName(toolName)}`,
// ...
function sanitizeMcpName(name: string): string {
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_");
return sanitized.length > 0 ? sanitized : "unknown";
}规则就两条:[a-zA-Z0-9_-] 以外的字符换成下划线,连续下划线压成一个;清洗后为空用 unknown。没有长度上限,也没有撞名检测,core 里另有一份同样的实现(apps/zcode-cli/packages/core/src/mcp/name.ts:14)。插件服务器键里的冒号同样被替换,plugin:foo:bar 的工具于是叫 mcp__plugin_foo_bar__<tool>。两个工具清洗后同名时,后注册的覆盖先注册的,只留一条 console.warn(apps/zcode-cli/packages/core/src/tool/registry.ts:46)。对照 MiniMax Code 的 MCP:那边限长 80、撞名加后缀,并用持久名册防止新来源继承旧名字。
createMcpToolEntry 再把描述投影成工具条目(apps/zcode-cli/packages/core/src/mcp/index.ts:102):
| MCP 描述 | ZCode 工具条目 |
|---|---|
description | 原样放进 metadata.description,随工具契约交给模型 |
inputSchema | 强制 type: "object";缺失时给空 properties 并允许任意附加字段(descriptor.ts:30) |
outputSchema | 读入但不用,条目统一挂 McpToolOutputJsonSchema:content、structuredContent、isError、_meta |
readOnlyHint、destructiveHint | 决定 readOnly、destructive;风险等级只读为 low、破坏性为 high、其余 medium(apps/zcode-cli/packages/core/src/mcp/index.ts:116) |
idempotentHint | 与只读任一成立即 concurrentSafe,可与其他工具并发 |
openWorldHint | 读入,未被使用 |
服务器 node_repl 的 js | 例外:副作用范围记为 system、风险 high,其余 MCP 一律记为 network(apps/zcode-cli/packages/core/src/mcp/index.ts:115) |
不论注解怎么写,needsApproval 恒为真(apps/zcode-cli/packages/core/src/mcp/index.ts:123)。超时不许调用方覆盖;结果预算是给模型 50000 字节、内联 100000 字节,超出从头截断(apps/zcode-cli/packages/core/src/mcp/index.ts:144)。权限键是 mcp,规则可以按工具名、输入与网络目标匹配(apps/zcode-cli/packages/core/src/mcp/index.ts:189)。于是 build 与 edit 模式下 MCP 调用总要询问(有允许规则时除外);Plan 模式反倒例外,非破坏性的 MCP 工具直接放行(apps/zcode-cli/packages/core/src/permission/service.ts:417),细节见权限模式与规则。会话级禁用名单(如 CLI 的 --disallowed-tools)在注册时就把命中的 MCP 工具滤掉(apps/zcode-cli/packages/core/src/mcp/index.ts:78)。
结果与图片
formatMcpToolResult 把结果转成模型内容(apps/zcode-cli/packages/core/src/mcp/index.ts:369):文本块原样保留;图片块变成带 data URL 的图片;音频不交给模型,只留一行带 MIME 类型的“已省略”说明;内嵌资源被整段序列化成文本;非空的 structuredContent 另起一块附在后面;isError 为真时加前缀 MCP tool returned an error:,除非结果在 _meta 里声明了 message-only。
图片还要过一道归一(apps/zcode-cli/packages/core/src/mcp/image-normalization.ts:27)。handler 必须在这一步处理,因为结果预算只看得到图片的占位文本,大图原样进请求会把 provider 的请求体撑爆(apps/zcode-cli/packages/core/src/mcp/index.ts:245)。内联上限是 base64 后 200 KiB(image-normalization.ts:21),超限时:
- 普通 MCP:原图存成会话级工件,模型看到的是一段说明加工件路径与 URI(
image-normalization.ts:170);没配工件存储就只留“已省略”说明。 - 宿主
node_repl的js:先用统一的图片端口压到 200 KiB 以内、长边不超过 2000 像素(image-normalization.ts:133、image-normalization.ts:25),压不下来再走上面的路;模型显式截的浏览器截图另存一份原图,并在图片之后补一行Browser screenshot saved to:加绝对路径,保持图片在前的顺序(image-normalization.ts:80)。
官方 Computer Use 的“帧”另有一条精确保真的路径,但开源版的判定函数恒为假(packages/zcode-cua/frame-contract.js:7),这条分支不会生效。
OAuth
http 与 sse 服务器支持两种 OAuth。client_credentials 直接交给 SDK 的 ClientCredentialsProvider(adapters/src/mcp/index.ts:1547)。authorization_code 则是隐式默认:只要没写 oauth、请求头里也没有 Authorization,就按授权码流程准备,由服务端的 401 与发现机制触发(adapters/src/mcp/index.ts:1851)。它拆成两段:
| 阶段 | 做什么 | 出处 |
|---|---|---|
| 被动 | 纯 AuthProvider:token() 读凭据,临近过期 30 秒内主动刷新;401 时 onUnauthorized() 刷新,不做发现、不注册客户端、不开回调端口 | apps/zcode-cli/packages/adapters/src/mcp/oauth-provider.ts:34、apps/zcode-cli/packages/adapters/src/mcp/oauth-credentials.ts:177 |
| 刷新 | 跨进程文件锁单飞,最多等 45 秒,避免多个进程拿同一个 refresh token 撞上轮换检测 | apps/zcode-cli/packages/adapters/src/mcp/oauth-refresh.ts:35、oauth-refresh.ts:69 |
| 交互 | 抢授权租约;领头者 listen(0) 起本机回调,没配静态 clientId 时每次重新动态注册客户端,用 SDK 的 auth() 走完发现、授权、换码;跟随者每 500 毫秒轮询结果 | oauth-interactive.ts:77、oauth-interactive.ts:121 |
| 扩权 | 403 带 insufficient_scope 时,scope 取配置、现有 token 与 challenge 三者并集,强制重新授权 | adapters/src/mcp/index.ts:1274 |
| 存储 | ~/.zcode/v2/credentials.json,键前缀 mcp:oauth: 加服务器名、地址与授权参数的哈希,CLI 与桌面端共用 | apps/zcode-cli/packages/adapters/src/auth/shared-credentials.ts:289、apps/zcode-cli/packages/adapters/src/mcp/oauth.ts:16 |
回调路径缺省为 /oauth/callback/mcp/<server>(oauth-interactive.ts:459)。领头者只发布授权地址,不自动打开浏览器,注释说那会打断当前操作(oauth-interactive.ts:405)。地址写进服务器状态的 authorization.authorizationUrl,由桌面端设置页展示;CLI 的 /mcp 输出与 TUI 侧栏都不显示它(apps/zcode-cli/packages/cli/src/command-center/handlers/mcp.ts:97、app-sidebar-mcp.tsx:104),所以从代码看,纯 CLI 下需要浏览器授权的服务器没有现成的授权入口。
官方 MCP
“官方 MCP”指由 ZCode 自家后端提供、按用户的 ZCode 登录与 Coding Plan 套餐鉴权的 MCP 服务。只有插件的 MCP 配置(.mcp.json 或清单里的 mcpServers)能声明它;用户配置的 schema 是严格模式,不认这个字段,写了整台服务器都会因校验失败被跳过(apps/zcode-cli/packages/adapters/src/config/schema.ts:94)。插件里的声明方式是:auth 写成 { "type": "zcode_official", "provider": "jwt_token" },传输只能是 http 或 stdio,不能与 oauth 同时出现,静态请求头里也不许带身份头(apps/zcode-cli/packages/adapters/src/plugins/mcp.ts:190、plugins/mcp.ts:267)。服务器归属(插件 id、原始键)由加载器生成,清单里写了也会被覆盖(apps/zcode-cli/packages/adapters/src/plugins/mcp-official-auth.ts:39)。身份头是这几个(packages/shared/src/official-mcp-auth.ts:18):
export const OFFICIAL_MCP_AUTH_HEADER_NAMES = {
authorization: "Authorization",
codingPlanAuthorization: "X-Bigmodel-Authorization",
targetType: "Bigmodel-Target-Type",
organization: "Bigmodel-Organization",
project: "Bigmodel-Project",
} as const;Authorization 携带 ZCode 登录 JWT,X-Bigmodel-Authorization 携带 MaaS 登录 JWT,套餐类型是 PERSONAL 或 TEAM,团队套餐再成对附上组织与项目(packages/services/src/official-mcp/officialMcpCredentials.ts:397)。信任判定只有一条:目标 origin 必须逐字等于当前 ZCode API 的 https origin 且不带用户名密码,另有环境变量 ZCODE_OFFICIAL_MCP_DEV_TRUSTED_ORIGINS 只放开本机 http 回环地址供自测(official-mcp-auth.ts:190、official-mcp-auth.ts:138)。两种传输的投递通道不同:
- http:适配器包一层 fetch,逐请求校验 origin、覆盖写入身份头,禁止跟随重定向;带了凭据的请求遇到 401 只重试一次,401、403 与 3xx 各自归类报错(
apps/zcode-cli/packages/adapters/src/mcp/official-auth.ts:95、official-auth.ts:297)。 - stdio:请求由插件进程自己发出,身份头随每条出站协议消息放进
_meta的com.zcode/official-mcp-auth键(apps/zcode-cli/packages/adapters/src/mcp/stdio-transport.ts:81、adapters/src/mcp/index.ts:1428),拿不到时也下发ok: false与原因。
Agent 进程不是身份的权威:身份头经反向请求 interaction/requestOfficialMcpAuthHeaders 向宿主索取,凭据不落运行时配置(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/official-mcp-auth-port.ts:1)。独立 CLI 没有这个端口,也没有可信 origin 表,http 形态的官方 MCP 因此在第一个请求就以 official_auth_unavailable 失败(adapters/src/mcp/index.ts:1487),stdio 形态收到的则是同名原因(adapters/src/mcp/index.ts:602)。只有 http 形态的官方工具被标记为 official,界面才会信任结果里的 quota_exceeded、coding_plan_required 并弹出相应提示(adapters/src/mcp/index.ts:1079、packages/shared/src/official-mcp-tool-error.ts:20);stdio 结果由插件自己产出,可以伪造,故不标记。开源仓库里没有带这种声明的插件实体,官方插件定义表中的 image-search 注明“认证仍由官方 MCP adapter 注入”(apps/zcode-cli/packages/bootstrap/src/app/official-plugin-definitions.ts:186)。账号与套餐见账号、Coding Plan 与闲时计划。
工作区级 MCP 的安全含义
根目录 NOTICE 的风险表里有这样一句(NOTICE.md:19):
当前 Agent 运行配置将工作区级 MCP 纳入自动连接范围;启用 MCP 并启动运行时时,可能使用配置中的命令、环境变量、认证头或 OAuth 连接服务。
对应的代码就是上面那个恒为空的 untrustedProjectMcpServers(apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:110)。把几处事实连起来看:
- 一个仓库只要在
zcode.json或.zcode/config.json里写了mcp.servers,打开会话(TUI 一挂载就算)时其中的 stdiocommand就会在工作区里以当前用户身份启动,没有任何确认。 - 工具审批管的是
tools/call;连接、协商、列工具和 OAuth 刷新都不经过它(NOTICE.md:42)。工作区 Hook 那套按摘要信任的机制也不覆盖 MCP 配置(NOTICE.md:18),见生命周期 Hooks 与工作区信任。 - 旧设计的痕迹还在:状态枚举里有
untrusted(mcp.port.ts:110),状态投影会给它配上“Project MCP server requires explicit connection before use.”的说明(apps/zcode-cli/packages/bootstrap/src/mcp-config.ts:211),连接池刷新时也会跳过它(pool.ts:130),只是名单为空,这些分支走不到。 - 能用的开关:
features.mcp: false全关;由于同名时用户压过项目,在用户配置里写一个同名且enabled: false的条目,从代码看就能压住项目里的那一台。
子 Agent 不自己连 MCP,只借用父会话启动快照里已连上的服务器,能调工具,改连接的操作一律被拒(apps/zcode-cli/packages/core/src/subagent/borrowed-mcp-port.ts:34);profile 声明必需、父会话却没连上的服务器会直接报错(apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:596),“必需”按模型可见名做不分大小写的包含匹配(apps/zcode-cli/packages/core/src/subagent/mcp-config.ts:4、apps/zcode-cli/packages/core/src/mcp/name.ts:19)。细节见子 Agent。
下一篇:node_repl、Browser Use 与 Computer Use——浏览器控制为什么只给模型一个 js 工具,宿主进程、bridge 与 Playwright 怎样串起来,以及开源版里 Computer Use 的占位实现。