zwcad-mcp 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -1
- package/runtime/zwcad2d/server.py +115 -5
- package/tools.json +28 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
中望CAD(ZWCAD)MCP 服务,让大模型通过 [MCP 协议](https://modelcontextprotocol.io)直接操控中望CAD 平台与中望机械CAD,完成绘图、标注、图层/块/样式管理,以及图框、标题栏、明细表(BOM)等机械操作。
|
|
4
4
|
|
|
5
|
-
**共计
|
|
5
|
+
**共计 41 个工具**:平台 27 个(不依赖机械环境)+ 机械 11 个(需中望机械CAD)+ 诊断 2 个 + 启动器侧 1 个。
|
|
6
6
|
|
|
7
7
|
> 启动器侧的 `zwcad_wait_ready` 在 Python 运行时尚未就绪时也可用,用于等待初始化完成。
|
|
8
8
|
> 诊断统一走 `zwcad_diagnose`:Python 已就绪时返回完整诊断,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zwcad-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "中望CAD(ZWCAD)MCP 服务:用自然语言驱动 ZWCAD 二维平台与中望机械 CAD,完成绘图、标注、图层管理与机械图框/BOM 操作",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"doctor": "node bridge.js --doctor",
|
|
23
23
|
"prepare-runtime": "node bridge.js --prepare",
|
|
24
|
+
"export-tools": "python scripts/export-tools.py",
|
|
24
25
|
"build:connector": "node scripts/build-connector.js",
|
|
25
26
|
"check:connector": "node scripts/build-connector.js --check",
|
|
26
27
|
"test:errorlog": "node scripts/test-errorlog.js",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
ZWCAD-2D MCP Server
|
|
3
3
|
|
|
4
4
|
一个 MCP 入口同时提供 ZWCAD 平台能力与可选的中望机械能力。
|
|
@@ -7,6 +7,7 @@ ZWCAD-2D MCP Server
|
|
|
7
7
|
import math
|
|
8
8
|
import sys
|
|
9
9
|
import os
|
|
10
|
+
import time
|
|
10
11
|
import logging
|
|
11
12
|
import importlib.util
|
|
12
13
|
import pythoncom
|
|
@@ -185,7 +186,13 @@ def _get_default_frame_size(standard_name):
|
|
|
185
186
|
_cad_conn_cache = None
|
|
186
187
|
|
|
187
188
|
|
|
188
|
-
def get_cad_connection(require_mechanical: bool = False):
|
|
189
|
+
def get_cad_connection(require_mechanical: bool = False, create_if_not_exists: bool = False):
|
|
190
|
+
"""获取 ZWCAD 连接。
|
|
191
|
+
|
|
192
|
+
require_mechanical: 需要机械扩展时传 True。
|
|
193
|
+
create_if_not_exists: ZWCAD 未运行时是否尝试主动拉起。
|
|
194
|
+
默认 False,只在已运行实例上连接;launch_cad 工具会传 True。
|
|
195
|
+
"""
|
|
189
196
|
global _cad_conn_cache
|
|
190
197
|
if _cad_conn_cache is not None:
|
|
191
198
|
try:
|
|
@@ -205,7 +212,7 @@ def get_cad_connection(require_mechanical: bool = False):
|
|
|
205
212
|
pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
|
|
206
213
|
except Exception:
|
|
207
214
|
pass
|
|
208
|
-
zcad_conn = ZwCAD()
|
|
215
|
+
zcad_conn = ZwCAD(create_if_not_exists=create_if_not_exists)
|
|
209
216
|
mech_conn = None
|
|
210
217
|
mechanical_class = _load_mechanical_class() if require_mechanical else None
|
|
211
218
|
if mechanical_class is not None:
|
|
@@ -231,6 +238,17 @@ def _ok(message: str = None, **data) -> dict:
|
|
|
231
238
|
return data
|
|
232
239
|
|
|
233
240
|
|
|
241
|
+
def _document_info(doc) -> dict:
|
|
242
|
+
"""返回文档的稳定字段,兼容尚未保存的新图纸。"""
|
|
243
|
+
return {
|
|
244
|
+
"name": doc.Name,
|
|
245
|
+
"full_name": doc.FullName,
|
|
246
|
+
"path": doc.Path,
|
|
247
|
+
"saved": doc.Saved,
|
|
248
|
+
"readonly": doc.ReadOnly,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
234
252
|
def _typelib_state(load: bool = False):
|
|
235
253
|
"""返回 (loaded, source, error):ZwmToolKit 类型库加载状态。"""
|
|
236
254
|
if load:
|
|
@@ -1876,10 +1894,62 @@ def zwcad_manage_view(action: str, name: str = None, params: dict = None) -> dic
|
|
|
1876
1894
|
return _err(f"视图管理({action})", e)
|
|
1877
1895
|
|
|
1878
1896
|
|
|
1897
|
+
@mcp.tool
|
|
1898
|
+
def zwcad_launch_cad(create_new_drawing: bool = False,
|
|
1899
|
+
wait_timeout: float = 30.0) -> dict:
|
|
1900
|
+
"""启动或连接 ZWCAD。
|
|
1901
|
+
|
|
1902
|
+
ZWCAD 未运行时主动拉起它,已运行时直接复用现有实例。
|
|
1903
|
+
其它工具都要求 ZWCAD 已启动,本工具是唯一能在未启动时主动建立连接的入口。
|
|
1904
|
+
|
|
1905
|
+
create_new_drawing: 启动后没有任何文档时,是否新建一张空白图纸。
|
|
1906
|
+
wait_timeout: 等待 ZWCAD COM 应用就绪的秒数,范围 1~120。
|
|
1907
|
+
"""
|
|
1908
|
+
try:
|
|
1909
|
+
logger.info(
|
|
1910
|
+
"tool_call zwcad_launch_cad create_new_drawing=%s wait_timeout=%s",
|
|
1911
|
+
create_new_drawing,
|
|
1912
|
+
wait_timeout,
|
|
1913
|
+
)
|
|
1914
|
+
timeout = max(1.0, min(float(wait_timeout), 120.0))
|
|
1915
|
+
deadline = time.monotonic() + timeout
|
|
1916
|
+
last_error = None
|
|
1917
|
+
|
|
1918
|
+
while True:
|
|
1919
|
+
try:
|
|
1920
|
+
zcad_conn, _ = get_cad_connection(create_if_not_exists=True)
|
|
1921
|
+
app = zcad_conn.app
|
|
1922
|
+
app.Visible = True
|
|
1923
|
+
document_count = app.Documents.Count
|
|
1924
|
+
if create_new_drawing and document_count == 0:
|
|
1925
|
+
app.Documents.Add()
|
|
1926
|
+
document_count = app.Documents.Count
|
|
1927
|
+
|
|
1928
|
+
result = {
|
|
1929
|
+
"application": app.Name,
|
|
1930
|
+
"visible": bool(app.Visible),
|
|
1931
|
+
"document_count": document_count,
|
|
1932
|
+
}
|
|
1933
|
+
if document_count > 0:
|
|
1934
|
+
result["active_document"] = _document_info(app.ActiveDocument)
|
|
1935
|
+
return _ok("ZWCAD 已启动并可用", data=result)
|
|
1936
|
+
except Exception as exc:
|
|
1937
|
+
last_error = exc
|
|
1938
|
+
# 连接失败后必须清缓存,否则下一轮仍拿到失效实例
|
|
1939
|
+
reset_cad_connection()
|
|
1940
|
+
if time.monotonic() >= deadline:
|
|
1941
|
+
raise TimeoutError(
|
|
1942
|
+
f"等待 ZWCAD 启动超时({timeout:g} 秒): {last_error}"
|
|
1943
|
+
) from last_error
|
|
1944
|
+
time.sleep(0.5)
|
|
1945
|
+
except Exception as e:
|
|
1946
|
+
return _err("启动 ZWCAD", e)
|
|
1947
|
+
|
|
1948
|
+
|
|
1879
1949
|
@mcp.tool
|
|
1880
1950
|
def zwcad_manage_document(action: str, params: dict = None) -> dict:
|
|
1881
1951
|
"""文档管理。action及params:
|
|
1882
|
-
new(无需params) | save:{file_path} | close:{[save_changes]}
|
|
1952
|
+
new(无需params) | open:{file_path,[read_only],[launch_if_needed]} | save:{file_path} | close:{[save_changes]}
|
|
1883
1953
|
info/list(无需params) | activate:{name}
|
|
1884
1954
|
export:{filename,[extension]} | import:{filename,[x,y,z,scale_factor]}
|
|
1885
1955
|
plot:{plot_file,[plot_config]}
|
|
@@ -1888,9 +1958,49 @@ def zwcad_manage_document(action: str, params: dict = None) -> dict:
|
|
|
1888
1958
|
wblock:{file_name,[selection_set_name]}"""
|
|
1889
1959
|
try:
|
|
1890
1960
|
logger.info("tool_call zwcad_manage_document action=%s", action)
|
|
1891
|
-
zcad_conn, _ = get_cad_connection()
|
|
1892
1961
|
p = params or {}
|
|
1893
1962
|
|
|
1963
|
+
# open 必须在取通用连接之前处理:它允许在 ZWCAD 未运行时一并拉起
|
|
1964
|
+
if action == "open":
|
|
1965
|
+
file_path = p.get("file_path")
|
|
1966
|
+
if not file_path or not isinstance(file_path, str):
|
|
1967
|
+
return _err("打开图纸", ValueError("必须提供 file_path"))
|
|
1968
|
+
file_path = os.path.expandvars(file_path)
|
|
1969
|
+
if not os.path.isabs(file_path):
|
|
1970
|
+
return _err("打开图纸", ValueError("file_path 必须是绝对路径"))
|
|
1971
|
+
file_path = os.path.abspath(file_path)
|
|
1972
|
+
if os.path.splitext(file_path)[1].lower() != ".dwg":
|
|
1973
|
+
return _err("打开图纸", ValueError("当前接口只允许打开 .dwg 文件"))
|
|
1974
|
+
if not os.path.isfile(file_path):
|
|
1975
|
+
return _err("打开图纸", FileNotFoundError(f"文件不存在: {file_path}"))
|
|
1976
|
+
|
|
1977
|
+
launch_if_needed = bool(p.get("launch_if_needed", True))
|
|
1978
|
+
zcad_conn, _ = get_cad_connection(create_if_not_exists=launch_if_needed)
|
|
1979
|
+
app = zcad_conn.app
|
|
1980
|
+
app.Visible = True
|
|
1981
|
+
normalized_target = os.path.normcase(os.path.normpath(file_path))
|
|
1982
|
+
|
|
1983
|
+
# 已打开同一张图时直接激活,避免重复打开导致多窗口
|
|
1984
|
+
for opened_doc in app.Documents:
|
|
1985
|
+
opened_path = getattr(opened_doc, "FullName", "") or ""
|
|
1986
|
+
if opened_path and os.path.normcase(os.path.normpath(opened_path)) == normalized_target:
|
|
1987
|
+
opened_doc.Activate()
|
|
1988
|
+
return _ok(
|
|
1989
|
+
"图纸已经打开,现已激活",
|
|
1990
|
+
already_open=True,
|
|
1991
|
+
data=_document_info(opened_doc),
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
opened_doc = app.Documents.Open(file_path, bool(p.get("read_only", False)))
|
|
1995
|
+
opened_doc.Activate()
|
|
1996
|
+
return _ok(
|
|
1997
|
+
f"成功打开图纸: {file_path}",
|
|
1998
|
+
already_open=False,
|
|
1999
|
+
data=_document_info(opened_doc),
|
|
2000
|
+
)
|
|
2001
|
+
|
|
2002
|
+
zcad_conn, _ = get_cad_connection()
|
|
2003
|
+
|
|
1894
2004
|
if action == "new":
|
|
1895
2005
|
new_doc = zcad_conn.app.Documents.Add()
|
|
1896
2006
|
return _ok(f"成功创建新图纸: {new_doc.Name}", document=new_doc.Name)
|
package/tools.json
CHANGED
|
@@ -583,9 +583,35 @@
|
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
},
|
|
586
|
+
{
|
|
587
|
+
"name": "zwcad_launch_cad",
|
|
588
|
+
"description": "启动或连接 ZWCAD。\n\nZWCAD 未运行时主动拉起它,已运行时直接复用现有实例。\n其它工具都要求 ZWCAD 已启动,本工具是唯一能在未启动时主动建立连接的入口。\n\ncreate_new_drawing: 启动后没有任何文档时,是否新建一张空白图纸。\nwait_timeout: 等待 ZWCAD COM 应用就绪的秒数,范围 1~120。",
|
|
589
|
+
"inputSchema": {
|
|
590
|
+
"properties": {
|
|
591
|
+
"create_new_drawing": {
|
|
592
|
+
"default": false,
|
|
593
|
+
"type": "boolean"
|
|
594
|
+
},
|
|
595
|
+
"wait_timeout": {
|
|
596
|
+
"default": 30.0,
|
|
597
|
+
"type": "number"
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
"type": "object"
|
|
601
|
+
},
|
|
602
|
+
"outputSchema": {
|
|
603
|
+
"additionalProperties": true,
|
|
604
|
+
"type": "object"
|
|
605
|
+
},
|
|
606
|
+
"_meta": {
|
|
607
|
+
"_fastmcp": {
|
|
608
|
+
"tags": []
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
586
612
|
{
|
|
587
613
|
"name": "zwcad_manage_document",
|
|
588
|
-
"description": "文档管理。action及params:\nnew(无需params) | save:{file_path} | close:{[save_changes]}\ninfo/list(无需params) | activate:{name}\nexport:{filename,[extension]} | import:{filename,[x,y,z,scale_factor]}\nplot:{plot_file,[plot_config]}\nregen:{[scope:0=AllViewports/1=ActiveViewport]}\nstart_undo/end_undo(无需params)\nwblock:{file_name,[selection_set_name]}",
|
|
614
|
+
"description": "文档管理。action及params:\nnew(无需params) | open:{file_path,[read_only],[launch_if_needed]} | save:{file_path} | close:{[save_changes]}\ninfo/list(无需params) | activate:{name}\nexport:{filename,[extension]} | import:{filename,[x,y,z,scale_factor]}\nplot:{plot_file,[plot_config]}\nregen:{[scope:0=AllViewports/1=ActiveViewport]}\nstart_undo/end_undo(无需params)\nwblock:{file_name,[selection_set_name]}",
|
|
589
615
|
"inputSchema": {
|
|
590
616
|
"properties": {
|
|
591
617
|
"action": {
|
|
@@ -1300,7 +1326,7 @@
|
|
|
1300
1326
|
},
|
|
1301
1327
|
{
|
|
1302
1328
|
"name": "zwcad_diagnose",
|
|
1303
|
-
"description": "
|
|
1329
|
+
"description": "诊断平台和机械后端,并给出不修改系统的排障建议。",
|
|
1304
1330
|
"inputSchema": {
|
|
1305
1331
|
"properties": {
|
|
1306
1332
|
"probe_cad": {
|