Files
cad-agent/verify_mcp_frame.py
T
gjm 3bd77d950e fix(frame): 未知工具错误码 -32601 改为 -32602,符合 MCP 规范
MCP 规范《Tools / Error Handling》把「未知工具」归为**协议错误**,
示例即为 {"code": -32602, "message": "Unknown tool: invalid_tool_name"}。
基座原先回 -32601(Method not found),语义偏 JSON-RPC 方法层。

- mcp_configParser.cpp: mcp_Tools::Call 改 -32601 → -32602,
  消息改为 "Unknown tool: <name>"。
- 无需改 mcp_server.cpp 的 HandleToolsCall:其协议错误白名单
  {-32700,-32600,-32601,-32602} 已含 -32602,分类逻辑不变。
- 同步更新 verify_mcp_frame.py 与 verify_stdio_bridge.py 的断言
  (未知「方法」仍是 -32601,语义正确,不动)。

验证(重建基座 + 重启 CAD 后实测):
- 直连 TCP:{"error":{"code":-32602,"message":"Unknown tool: __no_such_tool__"}}
- verify_mcp_frame.py 4/4 PASS;verify_stdio_bridge.py 24/24 PASS
- 回归:verify_annotate.py 9/9、dwg_review --check 正常
- MCP Inspector 冒烟:tools/list 26 个工具、tools/call get_drawing_info 正常
2026-10-14 06:21:46 +08:00

128 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""验证基座 B 最小集改动(换行分帧 / 大报文 / string id / 通知 / isError)。
前提:CAD 已打开并加载了重新构建的 cad_mcp_frame.arx,8080 端口在监听。
用法:
python verify_mcp_frame.py
"""
import json
import socket
import sys
HOST = "127.0.0.1"
PORT = 8080
def raw_send(payload_bytes, *, timeout=10.0):
"""发送一帧(已含 '\\n'),读回响应原始字节(读不到则为空)。"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((HOST, PORT))
sock.sendall(payload_bytes)
data = b""
while True:
try:
chunk = sock.recv(65536)
except socket.timeout:
break
if not chunk:
break
data += chunk
if b"\n" in data:
break
return data
finally:
sock.close()
def _call(payload):
return raw_send(json.dumps(payload).encode("utf-8") + b"\n")
def test_string_id():
resp = json.loads(_call({"jsonrpc": "2.0", "id": "abc-123", "method": "tools/list"}).decode())
assert resp.get("id") == "abc-123", f"id 未原样回显: {resp.get('id')!r}"
count = len(resp.get("result", {}).get("tools", []))
print(f"[PASS] string id 原样回显;tools={count}")
def test_large_request():
"""构造 >2KB 的 arguments;旧基座会截断成 -32700,新基座应正常返回 -32602。"""
big = {"blob": "x" * 5000}
resp = json.loads(
_call(
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {"name": "__no_such_tool__", "arguments": big},
}
).decode()
)
code = (resp.get("error") or {}).get("code")
assert code == -32602, f"期望 -32602(未知工具,协议错误),实际 {resp}"
print("[PASS] >2KB 请求未被截断(返回 -32602 而非 -32700)")
def test_notification_no_reply():
data = raw_send(
json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}).encode() + b"\n",
timeout=2.0,
)
assert data == b"", f"通知不应回包,却收到 {data[:120]!r}"
print("[PASS] 无 id 的通知未回包")
def test_tool_error_shape():
"""用不存在的 handle 触发工具执行错误,应为 result.isError=true。"""
resp = json.loads(
_call(
{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {"name": "get_entity_detail", "arguments": {"handle": "ZZZZZZ"}},
}
).decode()
)
result = resp.get("result")
if isinstance(result, dict) and result.get("isError") is True:
print("[PASS] 工具执行错误 -> result.isError=true")
elif resp.get("error"):
print(f"[WARN] 仍是顶层 error(code={resp['error'].get('code')}),isError 映射未生效")
else:
print(f"[?] 未预期响应: {resp}")
def main():
# 先探测端口
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
probe.settimeout(1.5)
try:
probe.connect((HOST, PORT))
except OSError as exc:
print(f"[X] {HOST}:{PORT} 未监听({exc})。请先启动已加载插件的 CAD。")
return 1
finally:
probe.close()
print(f"=== 验证基座 {HOST}:{PORT} ===")
failed = 0
for test in (test_string_id, test_large_request, test_notification_no_reply, test_tool_error_shape):
try:
test()
except AssertionError as exc:
print(f"[FAIL] {test.__name__}: {exc}")
failed += 1
except Exception as exc: # noqa: BLE001
print(f"[ERROR] {test.__name__}: {exc}")
failed += 1
print("=== 全部通过 ===" if failed == 0 else f"=== {failed} 项未通过 ===")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())