zwcad-mcp 0.2.3 → 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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  中望CAD(ZWCAD)MCP 服务,让大模型通过 [MCP 协议](https://modelcontextprotocol.io)直接操控中望CAD 平台与中望机械CAD,完成绘图、标注、图层/块/样式管理,以及图框、标题栏、明细表(BOM)等机械操作。
4
4
 
5
- **共计 40 个工具**:平台 26 个(不依赖机械环境)+ 机械 11 个(需中望机械CAD)+ 诊断 2 个 + 启动器侧 1 个。
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.2.3",
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": "诊断 MCP 服务的运行状态,并按需给出修复建议。\n\n分两种情形,工具名不变:\n- Python 运行时已就绪:返回完整诊断(平台 COM 可用性、活动图纸、机械扩展与 ZwmToolKit 类型库状态)。\n- Python 运行时未就绪:自动降级为启动器视角,返回运行时状态、Python 解释器路径与版本、最近错误记录(含 pip 镜像源、依赖清单、耗时),并明确标注哪些项目本次未检测。\n\n因此排查任何问题都从这里开始,无需另找工具。",
1329
+ "description": "诊断平台和机械后端,并给出不修改系统的排障建议。",
1304
1330
  "inputSchema": {
1305
1331
  "properties": {
1306
1332
  "probe_cad": {