feat(mcp): 去 ZCode 影子 —— mcp/server.mjs 改为通用 MCP 服务
## 目的
`mcp/server.mjs` 此前注释与行为都绑定 ZCode,接入端必须为 AgentMail 写
专用插件。去掉这层绑定后,任何支持 MCP 的宿主挂一行配置即可用:
{"command":"node","args":["…/mcp/server.mjs"],"env":{
"AGENTMAIL_GATEWAY_URL":…,"AGENTMAIL_AGENT_NAME":…,
"AGENTMAIL_AGENT_SECRET":…,"AGENTMAIL_MCP_PLATFORM":"my-host"}}
协议层(零依赖手写 stdio JSON-RPC)与 11 个邮件工具本就与宿主无关,
真正要动的只有 4 处耦合 + 工具面。
## 改动
**1. 移除执行类工具(`run_command` / `write_file`)**
它们的门禁(lib/action-tools.mjs + lib/approval.mjs + 落盘授权表)是为
ZCode headless 的**双进程审批**设计的:MCP 进程问人、ZCode 钩子进程等回答、
中间靠文件对齐。脱离该宿主后这套门禁的前提不成立,挂在通用服务上等于
提供一条**没有审批的旁路**。
`lib/` 里三个模块与 `hooks/` 源码保留(桌面模式的 ZCode 仍走它们),
只是 server.mjs 不再装载。
**2. platform 可配置**:`AGENTMAIL_MCP_PLATFORM`,默认 `mcp`,
空白值回落默认值。原先硬编码 `'zcode'`(两处)。
**3. 错误文案去宿主名**:不再让模型/人「去 ZCode 的插件设置里填写」,
改为说明设置 `AGENTMAIL_*` 环境变量。
**4. 提示词如实说能力**(src/prompt.mjs):原文案向模型承诺
`run_command`/`write_file` 可用并分档描述「会被请示 / 直接生效」。
工具移除后那变成**指向不存在工具的承诺** —— 模型会去找、把整轮浪费在
换名字重试上。改为明说「本平台没有执行面,需要动手就写进回信请人做」。
三档措辞仍互不相同(`plan`/`workspace`/`full`),因为「档位仍存在但都无
执行面」这件事模型需要知道。
## ★★ 顺带修掉一个真实缺陷(端到端撞出来的)
`connect_to_server` 对 secret-only 的 Agent **一直 400**:
`/agent/register` 只认 `Authorization: Bearer` 或 body 里的 `secret`,
不认 `X-Agent-Secret` 头(其它接口才认),而它漏了 `body.secret`。
dsh / pi 正是 secret-only 配置 ⇒ 它们调「连一下服务器」必然失败,
且模型看不出该改什么。
lib/gateway.mjs 的 `register()` 本来就做对了,tools.mjs 里是手抄的劣化副本。
修后实测 `HTTP 400` → `已连接 …(状态:registered)`。
## 判据
新增 `test/generic-mcp.test.mjs`(5 格)。**这三件事此前无人看守**:
变异验证时「把 action-tools 挂回 server.mjs」与「platform 硬编码回 zcode」
都能全套通过 —— 因为没有判据看 server.mjs 实际挂了什么、也没人看 platform。
改写的 4 格(prompt 3 格 + driver 1 格)保留原意图(不向模型撒谎、
native 自报要有真凭据、工具不存在时不要重试),改为断言新事实。
**变异验证**(每条都确认已应用后才数红格):
挂回 action-tools → 红 3
platform 硬编码 zcode → 红 3
platform 空白不回落 → 红 3
文案指回 ZCode 插件设置 → 红 3
删掉 body.secret(400 复现) → 红 3
全套 **402/402**。
## 端到端验收
写了一个**非 ZCode 宿主**探针(纯 stdio JSON-RPC,不加载任何插件),
对着真实网关跑通:initialize → tools/list(11 个,无执行类)→
connect_to_server(registered)→ suggest_address。
## 未做
- 未发布到 npm registry(`npx` 即用需要发布或指向仓库路径)。
- 未改 `check-deploy-drift.mjs` 的 zcode 豁免(本机仍不退场该宿主)。
This commit is contained in:
@ -11,7 +11,7 @@
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import { dirname, basename } from 'node:path';
|
||||
|
||||
/** 与网关一致的默认值;无头部署时由 ZCode 的 userConfig / 环境变量覆盖。 */
|
||||
/** 与网关一致的默认值;宿主配置优先,否则环境变量覆盖。 */
|
||||
const DEFAULT_BASE = 'http://127.0.0.1:8180';
|
||||
|
||||
export class GatewayError extends Error {
|
||||
@ -31,6 +31,9 @@ export class GatewayClient {
|
||||
this.agentKey = String(env.AGENTMAIL_AGENT_KEY || '').trim();
|
||||
this.agentSecret = String(env.AGENTMAIL_AGENT_SECRET || '').trim();
|
||||
this.agentName = String(env.AGENTMAIL_AGENT_NAME || '').trim();
|
||||
// 注册时上报的 platform(服务端按它统计在线桥,WebUI 会显示成平台名)。
|
||||
// 默认 'mcp' —— 本客户端是通用 MCP 服务器,不属于任何单一宿主。
|
||||
this.platform = String(env.AGENTMAIL_MCP_PLATFORM || 'mcp').trim() || 'mcp';
|
||||
}
|
||||
|
||||
/** 配置是否足以发请求 —— 缺密钥时要在第一次调用就明确报错,而不是收到 401 再猜。 */
|
||||
@ -67,7 +70,7 @@ export class GatewayClient {
|
||||
if (!this.agentKey && !this.agentSecret) {
|
||||
throw new Error('缺少 AGENTMAIL_AGENT_KEY 或 AGENTMAIL_AGENT_SECRET');
|
||||
}
|
||||
const body = { name: this.agentName, platform: 'zcode', ...extra };
|
||||
const body = { name: this.agentName, platform: this.platform, ...extra };
|
||||
if (!this.agentKey) body.secret = this.agentSecret;
|
||||
return this.post('/agent/register', body);
|
||||
}
|
||||
|
||||
@ -83,8 +83,9 @@ export async function handleMessage(msg, ctx) {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
// annotations 必须透传:ZCode 用它算风险等级(readOnlyHint→low /
|
||||
// destructiveHint→high),而 plan 档下「非破坏性的 MCP 工具直接放行」
|
||||
// annotations 必须透传:宿主据此算风险等级(readOnlyHint→low /
|
||||
// destructiveHint→high —— ZCode 的规则是逐字逆自其 CLI 产物),
|
||||
// 而 plan 档下「非破坏性的 MCP 工具直接放行」
|
||||
// 依赖它。漏传的后果不是「少个提示」,而是工具在该档下全被拒。
|
||||
...(t.annotations ? { annotations: t.annotations } : {})
|
||||
}))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 暴露给 ZCode 模型的 AgentMail 工具。
|
||||
* 暴露给 MCP 宿主的 AgentMail 工具。
|
||||
*
|
||||
* # 为什么工具集与另三个桥完全相同
|
||||
*
|
||||
@ -11,11 +11,12 @@
|
||||
* 一旦这里少一个参数或换一种说法,就会出现「某个平台上模型不会回信」这类
|
||||
* 只在单一平台复现的问题 —— 而排查时最费时间的正是「它到底和别的平台哪里不一样」。
|
||||
*
|
||||
* # 与平台无关
|
||||
* # 与宿主无关
|
||||
*
|
||||
* 本模块不认识 MCP,也不认识 ZCode:它只是一组
|
||||
* `{name, description, inputSchema, run(args) -> string}`。
|
||||
* 本模块不认识任何特定宿主(ZCode / Claude Desktop / codex / 各类 agent harness…),
|
||||
* 它只是一组 `{name, description, inputSchema, run(args) -> string}`。
|
||||
* 协议那层在 lib/mcp-rpc.mjs,入口在 mcp/server.mjs。
|
||||
* 配置一律走环境变量 `AGENTMAIL_*`,由宿主注入。
|
||||
*/
|
||||
|
||||
import {
|
||||
@ -88,7 +89,7 @@ export function buildTools({ client, agentName }) {
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`AgentMail 未配置完成:缺少 ${missing.join('、')}。` +
|
||||
`请在 ZCode 的插件设置里填写,或为 ZCode 进程设置同名环境变量。`
|
||||
`请设置 AGENTMAIL_AGENT_NAME / AGENTMAIL_AGENT_KEY(或 AGENTMAIL_AGENT_SECRET)环境变量后重启 MCP 服务器。`
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -439,7 +440,7 @@ export function buildTools({ client, agentName }) {
|
||||
if (!agentName || (!key && !client.agentSecret)) {
|
||||
return (
|
||||
`AgentMail 尚未配置完成:缺少 ${missing.join('、')}。\n` +
|
||||
`请在 ZCode 的插件设置里填写,或为 ZCode 进程设置同名环境变量后重启。\n` +
|
||||
`请设置 AGENTMAIL_AGENT_NAME / AGENTMAIL_AGENT_KEY(或 AGENTMAIL_AGENT_SECRET)环境变量后重启 MCP 服务器。\n` +
|
||||
`(当前解析到的 Gateway 地址:${url})`
|
||||
);
|
||||
}
|
||||
@ -451,7 +452,17 @@ export function buildTools({ client, agentName }) {
|
||||
? { Authorization: `Bearer ${key}` }
|
||||
: { 'X-Agent-Secret': client.agentSecret })
|
||||
},
|
||||
body: JSON.stringify({ name: agentName, platform: 'zcode' })
|
||||
// ★ 2026-10-02 修:secret-only 的 Agent(dsh/pi 就是这么配的)之前注册
|
||||
// **一直 400** —— 该端点只认 `Authorization: Bearer` 或 body 里的
|
||||
// `secret`,不认 `X-Agent-Secret` 头(其它接口才认)。这里漏了 body.secret,
|
||||
// 于是模型调「连一下服务器」就被 400 卡住,而它看不出该改什么。
|
||||
// lib/gateway.mjs 的 register() 本来就做对了(没密钥时把 secret 放进
|
||||
// body),之前只是没用上。
|
||||
body: JSON.stringify({
|
||||
name: agentName,
|
||||
platform: client.platform || 'mcp',
|
||||
...(key ? {} : { secret: client.agentSecret })
|
||||
})
|
||||
});
|
||||
const text = await res.text();
|
||||
let data = {};
|
||||
|
||||
Reference in New Issue
Block a user