标准 MCP 客户端(Claude Desktop / CodeBuddy / MCP Inspector)只实现 stdio 与 Streamable HTTP,无法直连 cad_mcp_frame 的自定义裸 TCP。新增 mcp_bridge 包在 客户端与基座之间做协议适配,CAD 侧零改动: - 合成基座缺失的生命周期:initialize(含协议版本协商、capabilities、serverInfo) 与 ping;notifications/* 按规范不回包。 - tools/list、tools/call 原样转发到 127.0.0.1:8080,result 与顶层 error 忠实透传, 仅把 id 改写回客户端原始 id。 - 并发:读线程即时应答 initialize/ping,单工作线程串行转发工具调用(保证顺序、 慢工具不阻塞 ping)。 - stdout 只走 sys.stdout.buffer 并加锁,规避 Windows 的 \n→\r\n 翻译; 诊断信息一律走 stderr。 - CAD 未启动时桥照常启动,工具调用回 -32603(可重试)。 - 入口:python -m mcp_bridge,支持 --host/--port/--timeout/--verbose/--print-config。 dwg_review/mcp_client.py 增加 raw_rpc()(_rpc 新增 normalize 开关), 返回基座原始响应 dict 供桥透传,避免重复实现 TCP 封装。 验证: - verify_stdio_bridge.py 24/24 PASS(版本协商、通知不回包、ping、tools/list 与基座 等价、tools/call、错误透传、-32700、string id、stdout 纯净性、死端口降级)。 - 真实标准客户端 MCP Inspector 冒烟通过:tools/list 拿到 26 个工具, tools/list --strict 退出码 0(无 schema 可移植性问题), tools/call get_drawing_info 返回标准 content + isError:false。 - 回归:verify_mcp_frame.py 4/4、verify_annotate.py 9/9、dwg_review --list-tools/--check 正常。
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""MCP 协议常量与纯函数。
|
|
|
|
本模块**不含任何 IO**,只做版本协商、信封构造与方法路由判定,便于单独测试。
|
|
"""
|
|
|
|
from . import __version__ as SERVER_VERSION
|
|
|
|
# 本桥支持的协议版本,从新到旧。initialize 时按客户端请求协商。
|
|
PROTOCOL_VERSIONS = ("2025-06-18", "2025-03-26", "2024-11-05")
|
|
DEFAULT_VERSION = PROTOCOL_VERSIONS[0]
|
|
|
|
SERVER_NAME = "cad-mcp-bridge"
|
|
|
|
# classify() 的四种路由结果
|
|
LOCAL = "local" # 由桥自己应答(基座没有实现)
|
|
FORWARD = "forward" # 原样转发给 CAD 基座
|
|
NOTIFY = "notify" # 通知,按规范不回包
|
|
UNKNOWN = "unknown" # 未知方法,回 -32601
|
|
|
|
_LOCAL_METHODS = frozenset({"initialize", "ping"})
|
|
_FORWARD_METHODS = frozenset({"tools/list", "tools/call"})
|
|
|
|
|
|
def negotiate_version(requested):
|
|
"""协议版本协商:客户端请求的版本受支持则回显,否则回本桥支持的最新版本。
|
|
|
|
依据 MCP 规范:服务端支持的版本必须原样返回;不支持时回退到自己支持的版本,
|
|
由客户端决定是否继续。
|
|
"""
|
|
return requested if requested in PROTOCOL_VERSIONS else DEFAULT_VERSION
|
|
|
|
|
|
def classify(method):
|
|
"""把方法名映射到路由动作。"""
|
|
if method in _LOCAL_METHODS:
|
|
return LOCAL
|
|
if method in _FORWARD_METHODS:
|
|
return FORWARD
|
|
if method.startswith("notifications/"):
|
|
return NOTIFY
|
|
return UNKNOWN
|
|
|
|
|
|
def ok(msg_id, result):
|
|
"""构造 JSON-RPC 成功响应。"""
|
|
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
|
|
|
|
|
def err(msg_id, code, message):
|
|
"""构造 JSON-RPC 错误响应。"""
|
|
return {"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
|
|
|
|
|
|
def initialize_result(params):
|
|
"""合成 initialize 的 result(基座未实现生命周期,由桥代答)。"""
|
|
requested = ""
|
|
if isinstance(params, dict):
|
|
requested = params.get("protocolVersion") or ""
|
|
return {
|
|
"protocolVersion": negotiate_version(requested),
|
|
"capabilities": {"tools": {"listChanged": False}},
|
|
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
"instructions": (
|
|
"本服务器通过 stdio 桥接本机 AutoCAD 的 cad_mcp_frame 基座。"
|
|
"工具仅在 CAD 已启动且已加载插件时可用;未启动时调用会返回连接错误。"
|
|
),
|
|
}
|