From 8b3033d9b48c5712f59f60e35278744bea0c0ac8 Mon Sep 17 00:00:00 2001 From: 6tizer <6tizer@gmail.com> Date: Sat, 8 Aug 2026 11:57:35 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(mcp-server):=20MCP=202026-07-28=20?= =?UTF-8?q?=E6=96=B0=E7=BA=AA=E5=85=83=20dual-era=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server/discover 探针 (supportedVersions/capabilities/缓存提示), 任意时刻可答 - 纪元路由: initialize → legacy; 带 modern _meta 信封 → modern; 探针不定纪元; 首个无信封请求 → legacy (保留 zcode auto 探测的回落语义) - modern 路径: _meta 信封必填校验 (缺 -32602), 版本不符 -32022 带 supported/requested; 所有 result 盖 resultType:complete + serverInfo 戳; list 类结果带 ttlMs/cacheScope (静态清单 1h/public) - legacy 路径零变化 (initialize 协商/tools/list 旧形状) - 互操作实测: 官方 Python SDK v2.0.0 client session.discover() 协商出 2026-07-28, tools/list + tools/call 全程新协议通过 - 新增 9 个 modern 纪元用例 (test_mcp_protocol.py 共 22 个); README/agent-help 同步 --- README.md | 2 +- packages/agent-help/zcode-agent-help | 2 +- packages/mcp-server/zcode-mcp-server | 113 ++++++++++++++++++++++++++- tests/test_mcp_protocol.py | 106 ++++++++++++++++++++++++- 4 files changed, 215 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3736e37..3f9157f 100644 --- a/README.md +++ b/README.md @@ -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(驱动说明书) diff --git a/packages/agent-help/zcode-agent-help b/packages/agent-help/zcode-agent-help index f9b1407..b1080cd 100755 --- a/packages/agent-help/zcode-agent-help +++ b/packages/agent-help/zcode-agent-help @@ -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+写工具物理禁用: 免授权只读)"}, diff --git a/packages/mcp-server/zcode-mcp-server b/packages/mcp-server/zcode-mcp-server index 86a2cb6..fbd23a9 100755 --- a/packages/mcp-server/zcode-mcp-server +++ b/packages/mcp-server/zcode-mcp-server @@ -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): @@ -1264,6 +1273,79 @@ 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 戳 (+缓存提示)。""" + result = dict(payload) + result["resultType"] = "complete" + result.setdefault("_meta", {})[_META_SERVER_INFO] = dict(SERVER_INFO) + if cacheable: + result["ttlMs"] = 3_600_000 # 工具集/版本清单静态, 可缓存 1h + result["cacheScope"] = "public" # 无用户特定数据 + 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 信封校验: protocolVersion + clientCapabilities 必填 (缺 → -32602) + 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 isinstance(caps, dict): + return make_error( + msg_id, -32602, + "Invalid params: modern 请求缺少必需的 _meta 信封 " + "(io.modelcontextprotocol/protocolVersion + 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}) + + if method == "server/discover": + return make_response(msg_id, _discover_result()) + 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") @@ -1315,9 +1397,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]}") + era = None # None=未判定 / "legacy" / "modern" (按 client 开场方式分流) # 按行读取 stdin for line in sys.stdin: line = line.strip() @@ -1332,7 +1417,6 @@ 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( @@ -1340,8 +1424,29 @@ def main(): "Invalid Request: 仅接受单个 JSON-RPC 对象")) continue + # ---- 纪元路由 (dual-era, 2026-08-08) ---- + # initialize → legacy; 带 modern _meta 信封 → modern; + # server/discover 任意时刻可答 (modern client 的时代探针, 不定纪元); + # 首个无信封的非通知请求 → legacy (zcode auto 探测的回落语义)。 + method = req.get("method") + meta = (req.get("params") or {}).get("_meta") or {} + is_notification = "id" not in req + if method == "initialize": + era = "legacy" + elif _META_PV in meta: + era = "modern" + elif method == "server/discover": + pass # 探针不改纪元 + elif era is None and not is_notification: + era = "legacy" + try: - resp = handle_request(req) + if method == "server/discover": + resp = make_response(req.get("id"), _discover_result()) + elif era == "modern": + resp = handle_modern_request(req) + else: + resp = handle_request(req) if resp is not None: send_message(resp) except Exception as e: diff --git a/tests/test_mcp_protocol.py b/tests/test_mcp_protocol.py index 4ba7eb7..8def1a9 100644 --- a/tests/test_mcp_protocol.py +++ b/tests/test_mcp_protocol.py @@ -106,9 +106,10 @@ def test_mr1_bare_value_rejected(self): self.assertEqual(responses[0]["error"]["code"], -32600) def test_mr2_unknown_method_32601(self): - """MR2: 未知 method → -32601 (zcode auto 探测的 legacy 回落信号, 勿改)""" + """MR2: 未知 method → -32601 (zcode auto 探测的 legacy 回落信号, 勿改。 + 注: server/discover 自 1.4.0 起正常应答, 不再属于'未知 method')""" responses = self._run_main([ - json.dumps({"jsonrpc": "2.0", "id": 9, "method": "server/discover"}), + json.dumps({"jsonrpc": "2.0", "id": 9, "method": "bogus/method"}), ]) self.assertEqual(responses[0]["error"]["code"], -32601) @@ -167,5 +168,106 @@ def test_tl3_tool_naming_convention(self): f"{t['name']} 不符合命名规范") +def _modern_req(method, rid=1, params=None, version="2026-07-28"): + """构造一条 modern 纪元请求 (带 _meta 信封)。""" + p = dict(params or {}) + p["_meta"] = { + "io.modelcontextprotocol/protocolVersion": version, + "io.modelcontextprotocol/clientCapabilities": {}, + } + return json.dumps({"jsonrpc": "2.0", "id": rid, "method": method, "params": p}) + + +class TestModernEra(_MainLoopCase): + """2026-07-28 新纪元 (dual-era) 行为""" + + def test_de0_discover_probe(self): + """DE0: server/discover 探针 → supportedVersions 含 2026-07-28, 带缓存提示""" + responses = self._run_main([_modern_req("server/discover")]) + result = responses[0]["result"] + self.assertIn("2026-07-28", result["supportedVersions"]) + self.assertIn("2024-11-05", result["supportedVersions"]) + self.assertEqual(result["resultType"], "complete") + self.assertIn("ttlMs", result) + self.assertEqual(result["cacheScope"], "public") + self.assertIn("io.modelcontextprotocol/serverInfo", result["_meta"]) + + def test_de1_modern_tools_list(self): + """DE1: modern tools/list → resultType + ttlMs/cacheScope + serverInfo 戳""" + responses = self._run_main([_modern_req("tools/list")]) + result = responses[0]["result"] + self.assertEqual(len(result["tools"]), 4) + self.assertEqual(result["resultType"], "complete") + self.assertIn("ttlMs", result) + self.assertIn("cacheScope", result) + + def test_de2_modern_tools_call(self): + """DE2: modern tools/call → 结果带 resultType 与 serverInfo 戳""" + responses = self._run_main([_modern_req( + "tools/call", + params={"name": "get_zcode_capabilities", "arguments": {"section": "ecosystem"}})]) + result = responses[0]["result"] + self.assertEqual(result["resultType"], "complete") + self.assertIn("zcode-mcp-server", result["content"][0]["text"]) + self.assertIn("io.modelcontextprotocol/serverInfo", result["_meta"]) + + def test_de3_missing_envelope_32602(self): + """DE3: 已进入 modern 后缺信封的请求 → -32602""" + responses = self._run_main([ + _modern_req("server/discover"), # 不定纪元 + _modern_req("tools/list"), # 定 modern + json.dumps({"jsonrpc": "2.0", "id": 7, # 无信封 + "method": "tools/list", "params": {}}), + ]) + self.assertEqual(responses[2]["error"]["code"], -32602) + + def test_de4_unsupported_version_32022(self): + """DE4: modern 信封里版本不认识 → -32022 带 supported/requested""" + responses = self._run_main([_modern_req("tools/list", version="2099-01-01")]) + err = responses[0]["error"] + self.assertEqual(err["code"], -32022) + self.assertIn("2026-07-28", err["data"]["supported"]) + self.assertEqual(err["data"]["requested"], "2099-01-01") + + def test_de5_probe_then_legacy_initialize(self): + """DE5: 先探 discover 再走 legacy initialize → 两纪元并存 (dual-era 灵活性)""" + responses = self._run_main([ + _modern_req("server/discover"), + json.dumps({"jsonrpc": "2.0", "id": 2, "method": "initialize", + "params": {"protocolVersion": "2025-11-25"}}), + json.dumps({"jsonrpc": "2.0", "id": 3, "method": "tools/list"}), + ]) + self.assertIn("supportedVersions", responses[0]["result"]) + self.assertEqual(responses[1]["result"]["protocolVersion"], "2025-11-25") + # legacy tools/list 不带 resultType (老 client 预期旧形状) + self.assertNotIn("resultType", responses[2]["result"]) + + def test_de6_pure_legacy_flow_unaffected(self): + """DE6: 纯 legacy 流程 (initialize 开场) 完全不受 modern 影响""" + responses = self._run_main([ + json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-06-18"}}), + json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}), + json.dumps({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}), + ]) + self.assertEqual(responses[0]["result"]["protocolVersion"], "2025-06-18") + self.assertNotIn("resultType", responses[1]["result"]) + self.assertNotIn("ttlMs", responses[1]["result"]) + + def test_de7_modern_unknown_method_32601(self): + """DE7: modern 纪元未知 method → -32601""" + responses = self._run_main([_modern_req("resources/list")]) + self.assertEqual(responses[0]["error"]["code"], -32601) + + def test_de8_modern_notification_silent(self): + """DE8: modern notification (cancelled) 静默吞掉不应答""" + responses = self._run_main([ + _modern_req("tools/list"), + json.dumps({"jsonrpc": "2.0", "method": "notifications/cancelled", + "params": {"requestId": 1, "reason": "user"}}), + ]) + self.assertEqual(len(responses), 1, "notification 不应有响应") + + if __name__ == "__main__": unittest.main(verbosity=2) From 9baa024cdf661985d9ddcc842c0e98e700051f8a Mon Sep 17 00:00:00 2001 From: 6tizer <6tizer@gmail.com> Date: Sat, 8 Aug 2026 12:03:43 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(mcp-server):=20dual-era=20=E7=8B=97?= =?UTF-8?q?=E9=A3=9F=20review=20P0/P1=20=E4=BF=AE=E5=A4=8D=E9=97=AD?= =?UTF-8?q?=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P0-1: 纪元路由从会话级状态机改为逐请求判定 (无会话态) — 修掉 'legacy 请求偶发带 _meta 毒死会话'与双轨横跳; 与新协议无状态哲学一致 - P0-2: server/discover 作 notification 时不应答 (不再发 id:null 伪响应) - P1: 信封校验报错拆分 (缺 pv/缺 caps 分别精确文案); _modern_result 的 _meta 浅拷贝不就地改调用方对象; discover 统一为中立探针 (main 先行应答, handle_modern_request 不再重复分支); legacy handler 对未知 notification 静默 (JSON-RPC 合规) - P2: resultType/缓存提示注释约束; discover 版本列表两纪元混列保持 spec 形状并注释; 新增 SDK v2 真实抓包序列回放契约测试 (DE9) - 测试 26 全绿; 官方 Python SDK v2.0.0 互操作复测通过 (discover 协商出 2026-07-28, list/call 全程新协议) --- packages/mcp-server/zcode-mcp-server | 67 +++++++++++++++---------- tests/test_mcp_protocol.py | 74 +++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 33 deletions(-) diff --git a/packages/mcp-server/zcode-mcp-server b/packages/mcp-server/zcode-mcp-server index fbd23a9..43d4d88 100755 --- a/packages/mcp-server/zcode-mcp-server +++ b/packages/mcp-server/zcode-mcp-server @@ -1282,13 +1282,21 @@ def make_error(msg_id, code, message, data=None): # 纪元路由在 main() 的读循环里 (initialize → legacy, modern 信封 → modern)。 # ============================================================ def _modern_result(payload, cacheable=False): - """modern 纪元 result 封装: resultType 必填 + serverInfo 戳 (+缓存提示)。""" + """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" - result.setdefault("_meta", {})[_META_SERVER_INFO] = dict(SERVER_INFO) if cacheable: result["ttlMs"] = 3_600_000 # 工具集/版本清单静态, 可缓存 1h - result["cacheScope"] = "public" # 无用户特定数据 + result["cacheScope"] = "public" # 无用户特定数据 (动态化时改 private) return result @@ -1309,22 +1317,25 @@ def handle_modern_request(req): if msg_id is None: return None - # _meta 信封校验: protocolVersion + clientCapabilities 必填 (缺 → -32602) + # _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 isinstance(caps, dict): + 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: modern 请求缺少必需的 _meta 信封 " - "(io.modelcontextprotocol/protocolVersion + clientCapabilities)") + "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}) - if method == "server/discover": - return make_response(msg_id, _discover_result()) + # 注: server/discover 不走这里 — main 循环把它当中立探针先行应答 + # (不定纪元、不校验信封), 两条路径行为一致 (P1-4)。 if method == "tools/list": return make_response(msg_id, _modern_result({"tools": TOOLS}, cacheable=True)) if method == "tools/call": @@ -1370,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}) @@ -1402,8 +1418,7 @@ def main(): f"{MODERN_PROTOCOL_VERSIONS[0]}") log(f"暴露 tools: {[t['name'] for t in TOOLS]}") - era = None # None=未判定 / "legacy" / "modern" (按 client 开场方式分流) - # 按行读取 stdin + # 按行读取 stdin (纪元逐请求判定, 无会话状态) for line in sys.stdin: line = line.strip() if not line: @@ -1424,29 +1439,27 @@ def main(): "Invalid Request: 仅接受单个 JSON-RPC 对象")) continue - # ---- 纪元路由 (dual-era, 2026-08-08) ---- - # initialize → legacy; 带 modern _meta 信封 → modern; - # server/discover 任意时刻可答 (modern client 的时代探针, 不定纪元); - # 首个无信封的非通知请求 → legacy (zcode auto 探测的回落语义)。 + # ---- 纪元路由 (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 - if method == "initialize": - era = "legacy" - elif _META_PV in meta: - era = "modern" - elif method == "server/discover": - pass # 探针不改纪元 - elif era is None and not is_notification: - era = "legacy" try: if method == "server/discover": - resp = make_response(req.get("id"), _discover_result()) - elif era == "modern": - resp = handle_modern_request(req) + 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_request(req) + resp = handle_modern_request(req) # modern 路径 (含信封校验) if resp is not None: send_message(resp) except Exception as e: diff --git a/tests/test_mcp_protocol.py b/tests/test_mcp_protocol.py index 8def1a9..5a428c3 100644 --- a/tests/test_mcp_protocol.py +++ b/tests/test_mcp_protocol.py @@ -211,15 +211,77 @@ def test_de2_modern_tools_call(self): self.assertIn("zcode-mcp-server", result["content"][0]["text"]) self.assertIn("io.modelcontextprotocol/serverInfo", result["_meta"]) - def test_de3_missing_envelope_32602(self): - """DE3: 已进入 modern 后缺信封的请求 → -32602""" + def test_de3_envelope_less_falls_to_legacy(self): + """DE3: 逐请求路由 (P0-1 修复): 无信封请求永远走 legacy, 不存在 + '会话被切到 modern 后无信封请求被 -32602 毒死'""" responses = self._run_main([ - _modern_req("server/discover"), # 不定纪元 - _modern_req("tools/list"), # 定 modern - json.dumps({"jsonrpc": "2.0", "id": 7, # 无信封 + _modern_req("tools/list", rid=1), # modern + json.dumps({"jsonrpc": "2.0", "id": 2, # 无信封 → legacy 形状 "method": "tools/list", "params": {}}), + _modern_req("tools/list", rid=3), # 再回 modern, 无横跳问题 ]) - self.assertEqual(responses[2]["error"]["code"], -32602) + self.assertEqual(responses[0]["result"]["resultType"], "complete") + self.assertNotIn("resultType", responses[1]["result"]) + self.assertEqual(responses[2]["result"]["resultType"], "complete") + + def test_de3b_partial_envelope_32602(self): + """DE3b: 信装有 protocolVersion 但缺 clientCapabilities → -32602 精确文案""" + bad = json.dumps({"jsonrpc": "2.0", "id": 5, "method": "tools/list", + "params": {"_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}) + responses = self._run_main([bad]) + self.assertEqual(responses[0]["error"]["code"], -32602) + self.assertIn("clientCapabilities", responses[0]["error"]["message"]) + + def test_de3c_discover_notification_no_response(self): + """DE3c: server/discover 作 notification (无 id) → 不应答 (P0-2)""" + responses = self._run_main([ + json.dumps({"jsonrpc": "2.0", "method": "server/discover"}), + _modern_req("server/discover", rid=1), + ]) + self.assertEqual(len(responses), 1, "notification 形式的 discover 不应有响应") + self.assertIn("supportedVersions", responses[0]["result"]) + + def test_de3d_discover_bogus_version_still_answered(self): + """DE3d: discover 带非法版本信封也照答 (中立探针不校验信封, P1-4)""" + responses = self._run_main([_modern_req("server/discover", version="bogus")]) + self.assertIn("supportedVersions", responses[0]["result"]) + + def test_de9_sdk_wire_fixtures(self): + """DE9: 官方 Python SDK v2.0.0 真实抓包序列的回放契约测试 (P2-4)""" + sdk_discover = json.dumps({ + "jsonrpc": "2.0", "id": 1, "method": "server/discover", + "params": {"_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": {"name": "mcp", "version": "0.1.0"}, + "io.modelcontextprotocol/clientCapabilities": {}}}}) + sdk_tools_list = json.dumps({ + "jsonrpc": "2.0", "id": 2, "method": "tools/list", + "params": {"_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": {"name": "mcp", "version": "0.1.0"}, + "io.modelcontextprotocol/clientCapabilities": {}}}}) + sdk_tools_call = json.dumps({ + "jsonrpc": "2.0", "id": 3, "method": "tools/call", + "params": {"name": "get_zcode_capabilities", + "arguments": {"section": "ecosystem"}, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": {"name": "mcp", "version": "0.1.0"}, + "io.modelcontextprotocol/clientCapabilities": {}}}}) + responses = self._run_main([sdk_discover, sdk_tools_list, sdk_tools_call]) + # 与 SDK 互操作实测时的期望形状逐点对齐 + r0 = responses[0]["result"] + self.assertIn("2026-07-28", r0["supportedVersions"]) + self.assertEqual(r0["resultType"], "complete") + self.assertEqual(r0["ttlMs"], 3600000) + self.assertEqual(r0["cacheScope"], "public") + self.assertEqual(r0["_meta"]["io.modelcontextprotocol/serverInfo"]["name"], + "zcode-mcp-server") + self.assertEqual(len(responses[1]["result"]["tools"]), 4) + self.assertEqual(responses[2]["result"]["resultType"], "complete") + self.assertIn("zcode-mcp-server", + responses[2]["result"]["content"][0]["text"]) def test_de4_unsupported_version_32022(self): """DE4: modern 信封里版本不认识 → -32022 带 supported/requested"""