Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ ZCODE_BASE_URL=https://api.z.ai/api/anthropic ./packages/mcp-server/zcode-mcp-se

**已对齐 2025-11-25 的义务**:拒收 JSON-RPC batch(2025-06-18 起规范移除,回 `-32600`)、tools/list 确定性顺序、tool `title` + `annotations`(`readOnlyHint` 等)元数据、输入校验错误走 `isError: true` 而非协议错误。

**路线图**:2026-07-28 新纪元(无握手无状态、`server/discover` 必实现、`resultType` 必填)将以 **dual-era** 形态评估接入——保留 initialize 旧路径服务存量 client,新增新协议路径(zcode 0.16.1 已是双纪元 client,可立即受益)。等 Claude Code / Kimi / Cursor 跟进新协议后再全面实施
**路线图**:~~2026-07-28 新纪元~~ **已完成(2026-08-08,dual-era 上线)**:server 同时服务两个纪元——`initialize` 开场走 legacy(2024-11-05~2025-11-25 协商),带 modern `_meta` 信封的请求走 2026-07-28 无状态新协议(`server/discover` 探针、信封缺失 `-32602`、版本不符 `-32022` 带 supported 列表、result 盖 `resultType`/`serverInfo` 戳、list 结果带 `ttlMs`/`cacheScope`)。已通过官方 Python SDK v2.0.0 client 互操作实测(`session.discover()` 协商出 2026-07-28,tools/list、tools/call 全程新协议);zcode 0.16.1 的 auto 探测也会自动走新协议。MRTR/elicitation/tasks/subscriptions 按调研结论不实现(废弃或用不上)

## Skill(驱动说明书)

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-help/zcode-agent-help
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ ECOSYSTEM = {
"role": "把 zcode 能力暴露为标准 MCP server",
"maturity": "stable",
"transport": "MCP over stdio (JSON-RPC 2.0)",
"protocol": "legacy 协商 2024-11-05..2025-11-25 (回显 client 版本); 2026-07-28 新纪元 dual-era 评估中",
"protocol": "dual-era: legacy 协商 2024-11-05..2025-11-25 (回显) + modern 2026-07-28 (server/discover + _meta 信封)",
"tools_exposed": [
{"name": "get_zcode_capabilities", "description": "返回 zcode 能力清单 (调 agent-help)"},
{"name": "zcode_review", "description": "调 zcode 审查代码 (yolo+写工具物理禁用: 免授权只读)"},
Expand Down
128 changes: 123 additions & 5 deletions packages/mcp-server/zcode-mcp-server
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,19 @@ SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-1
_SUPPORTED_PV_SET = set(SUPPORTED_PROTOCOL_VERSIONS) # 协商查表用 (list 保留给可读性/有序)
PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] # 我们支持的最高版本

# 2026-07-28 新纪元 (无状态化): 每请求 _meta 信封 + server/discover 探针。
# 双纪元路由在 main(): initialize → legacy; 带 modern 信封 → modern;
# server/discover 任意时刻可答 (它是 modern client 的时代探针)。
MODERN_PROTOCOL_VERSIONS = ["2026-07-28"]
ALL_SUPPORTED_VERSIONS = MODERN_PROTOCOL_VERSIONS + SUPPORTED_PROTOCOL_VERSIONS
_META_PV = "io.modelcontextprotocol/protocolVersion"
_META_CAPS = "io.modelcontextprotocol/clientCapabilities"
_META_SERVER_INFO = "io.modelcontextprotocol/serverInfo"

# 作为 MCP client 调 mimosa 时用的版本: mimosa 1.0.3 实测讲 2024-11-05,
# 钉死该值 (它不回更高版; 我们不校验它的应答版本)。
MIMOSA_CLIENT_PROTOCOL_VERSION = "2024-11-05"
SERVER_INFO = {"name": "zcode-mcp-server", "version": "1.3.0"}
SERVER_INFO = {"name": "zcode-mcp-server", "version": "1.4.0"}


def log(msg):
Expand Down Expand Up @@ -1264,6 +1273,90 @@ def make_error(msg_id, code, message, data=None):
return {"jsonrpc": "2.0", "id": msg_id, "error": err}


# ============================================================
# 2026-07-28 新纪元 (modern, 无状态) 请求处理
# ------------------------------------------------------------
# 与 legacy 的差异: 无 initialize 握手; 每个请求自带 _meta 信封
# (protocolVersion + clientCapabilities 必填); 所有 result 带 resultType;
# list 类结果带 ttlMs/cacheScope; server/discover 为时代探针。
# 纪元路由在 main() 的读循环里 (initialize → legacy, modern 信封 → modern)。
# ============================================================
def _modern_result(payload, cacheable=False):
"""modern 纪元 result 封装: resultType 必填 + serverInfo 戳 (+缓存提示)。

当前不支持 delta 流式分片, resultType 统一 "complete" (含 isError 的工具
执行错误, 那是完整结果不是中间态)。cacheable=True 仅用于静态清单
(tools/list、discover); 若未来工具集变动态, 须改 cacheScope=private
并调低 ttlMs。
"""
result = dict(payload)
meta = dict(result.get("_meta") or {}) # 浅拷贝, 不就地改调用方对象 (P1-3)
meta[_META_SERVER_INFO] = dict(SERVER_INFO)
result["_meta"] = meta
result["resultType"] = "complete"
if cacheable:
result["ttlMs"] = 3_600_000 # 工具集/版本清单静态, 可缓存 1h
result["cacheScope"] = "public" # 无用户特定数据 (动态化时改 private)
return result


def _discover_result():
"""server/discover 应答: 支持版本 + 能力 + serverInfo (modern 时代探针)。"""
return _modern_result({
"supportedVersions": ALL_SUPPORTED_VERSIONS,
"capabilities": {"tools": {"listChanged": False}},
}, cacheable=True)


def handle_modern_request(req):
"""处理一条 modern 纪元 (2026-07-28) 请求, 返回响应 dict 或 None。"""
msg_id = req.get("id")
method = req.get("method")

# notification (无 id) 静默吞掉 (cancelled 等); 信封校验只针对 request
if msg_id is None:
return None

# _meta 信封校验: 拆分报错, 缺哪个说哪个 (P1-2)
meta = (req.get("params") or {}).get("_meta") or {}
pv = meta.get(_META_PV)
caps = meta.get(_META_CAPS)
if not isinstance(pv, str) or not pv:
return make_error(
msg_id, -32602,
"Invalid params: _meta 缺 io.modelcontextprotocol/protocolVersion (非空字符串)")
if not isinstance(caps, dict):
return make_error(
msg_id, -32602,
"Invalid params: _meta 缺 io.modelcontextprotocol/clientCapabilities (对象)")
if pv not in MODERN_PROTOCOL_VERSIONS:
return make_error(
msg_id, -32022, f"Unsupported protocol version: {pv}",
data={"supported": ALL_SUPPORTED_VERSIONS, "requested": pv})

# 注: server/discover 不走这里 — main 循环把它当中立探针先行应答
# (不定纪元、不校验信封), 两条路径行为一致 (P1-4)。
if method == "tools/list":
return make_response(msg_id, _modern_result({"tools": TOOLS}, cacheable=True))
if method == "tools/call":
params = req.get("params", {})
tool_name = params.get("name")
arguments = params.get("arguments", {})
if tool_name not in TOOL_HANDLERS:
return make_error(msg_id, -32601, f"未知 tool: {tool_name}")
try:
result = TOOL_HANDLERS[tool_name](arguments)
# isError 也是 complete (工具执行错误走 isError, 非协议错误)
return make_response(msg_id, _modern_result(result))
except Exception as e:
log(f"tool {tool_name} 异常: {e}")
return make_error(msg_id, -32603, f"tool 执行异常: {e}")

if method is not None:
return make_error(msg_id, -32601, f"未知 method: {method}")
return None


def handle_request(req):
"""处理一条 JSON-RPC 请求,返回响应 dict (或 None 表示无需响应)"""
msg_id = req.get("id")
Expand All @@ -1288,6 +1381,11 @@ def handle_request(req):
if method == "notifications/initialized":
return None

# 其他 notification (无 id) 一律静默吞掉 — JSON-RPC 规定 notification
# 不应答 (回 id:null 的错误是协议违规, 会污染 client 响应队列)
if msg_id is None:
return None

# --- 工具发现 ---
if method == "tools/list":
return make_response(msg_id, {"tools": TOOLS})
Expand Down Expand Up @@ -1315,10 +1413,12 @@ def handle_request(req):


def main():
log(f"启动 zcode-mcp-server (stdio), protocol {PROTOCOL_VERSION}")
log(f"启动 zcode-mcp-server (stdio), dual-era: legacy "
f"{SUPPORTED_PROTOCOL_VERSIONS[-1]}..{PROTOCOL_VERSION} + modern "
f"{MODERN_PROTOCOL_VERSIONS[0]}")
log(f"暴露 tools: {[t['name'] for t in TOOLS]}")

# 按行读取 stdin
# 按行读取 stdin (纪元逐请求判定, 无会话状态)
for line in sys.stdin:
line = line.strip()
if not line:
Expand All @@ -1332,16 +1432,34 @@ def main():

# 非对象消息拒收: batch 数组 (2025-03-26 加入、2025-06-18 移除,
# ≥2025-06-18 的 server 必须拒绝) 和其他非 dict 形态, 统一 -32600。
# 注意: 对未知 method 回 -32601 是 zcode auto 探测依赖的回落信号, 别改。
if not isinstance(req, dict):
log("收到非 JSON-RPC 对象消息 (batch?), 拒绝")
send_message(make_error(
None, -32600,
"Invalid Request: 仅接受单个 JSON-RPC 对象"))
continue

# ---- 纪元路由 (dual-era, 逐请求判定, 无会话状态) ----
# 狗食 review P0-1: 早期的会话级 era 变量会被"legacy 请求偶发带 _meta"
# 毒死整个会话, 且双向覆写会造成 result 形状反复横跳 — 改为逐请求判定:
# server/discover → 中立探针, 直接答 (不定纪元, 不校验信封);
# initialize → legacy 握手 (新纪元没有它);
# 带 io.modelcontextprotocol/protocolVersion 信封 → modern;
# 其余 → legacy (zcode auto 探测回落语义)。
# 逐请求路由与 2026-07-28 的无状态哲学一致, 天然无互串。
method = req.get("method")
meta = (req.get("params") or {}).get("_meta") or {}
is_notification = "id" not in req

try:
resp = handle_request(req)
if method == "server/discover":
if is_notification:
continue # P0-2: notification 不应答 (否则发 id:null 伪响应)
resp = make_response(req["id"], _discover_result())
elif method == "initialize" or _META_PV not in meta:
resp = handle_request(req) # legacy 路径
else:
resp = handle_modern_request(req) # modern 路径 (含信封校验)
if resp is not None:
send_message(resp)
except Exception as e:
Expand Down
Loading
Loading