zwcad-mcp 0.1.0 → 0.2.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
@@ -29,26 +29,38 @@ MCP 客户端 ──stdio──► bridge.js ──spawn──► python -m zwca
29
29
 
30
30
  ## 快速开始
31
31
 
32
- 无需安装,客户端会自动拉起:
32
+ 无需安装,客户端会自动拉起。把下面整段复制进 MCP 客户端的配置文件即可。
33
+
34
+ ### 方案一:国内镜像(推荐国内用户)
33
35
 
34
36
  ```json
35
37
  {
36
38
  "mcpServers": {
37
39
  "zwcad": {
38
40
  "command": "npx",
39
- "args": ["-y", "zwcad-mcp"],
41
+ "args": ["--registry=https://registry.npmmirror.com", "-y", "zwcad-mcp"],
40
42
  "env": { "PYTHONUTF8": "1" }
41
43
  }
42
44
  }
43
45
  }
44
46
  ```
45
47
 
46
- 国内网络建议加镜像参数:
48
+ ### 方案二:npm 官方源
47
49
 
48
50
  ```json
49
- "args": ["--registry=https://registry.npmmirror.com", "-y", "zwcad-mcp"]
51
+ {
52
+ "mcpServers": {
53
+ "zwcad": {
54
+ "command": "npx",
55
+ "args": ["-y", "zwcad-mcp"],
56
+ "env": { "PYTHONUTF8": "1" }
57
+ }
58
+ }
59
+ }
50
60
  ```
51
61
 
62
+ 两者只有 `args` 里的 `--registry` 参数不同,功能完全一致。国内网络用方案一可显著加快首次拉取。
63
+
52
64
  各平台的配置文件位置见 [`connector/`](connector/) 目录。
53
65
 
54
66
  ## 验证接入
package/bridge.js CHANGED
@@ -91,12 +91,16 @@ function fatal(err) {
91
91
 
92
92
  /**
93
93
  * 同步执行并捕获 stdout,失败返回 null(用于探测,不抛异常)。
94
+ *
95
+ * 超时给得比较宽松:Windows 上首次启动 python.exe 可能被杀毒软件扫描,
96
+ * 实测出现过同一路径偶发 20 秒超时被误判为「不可用」的情况。
97
+ * 探测失败会直接导致挑不到可用解释器,代价远大于多等几秒。
94
98
  */
95
99
  function tryRun(exe, args, options) {
96
100
  try {
97
101
  const r = spawnSync(exe, args, {
98
102
  encoding: 'utf8',
99
- timeout: 20000,
103
+ timeout: 60000,
100
104
  windowsHide: true,
101
105
  ...options,
102
106
  });
@@ -136,12 +140,23 @@ function runStreaming(exe, args, label) {
136
140
 
137
141
  /**
138
142
  * 返回 Python 主次版本号,例如 [3, 13];不可用返回 null。
143
+ *
144
+ * 失败时重试一次:Windows 上 python.exe 首次启动偶尔会异常缓慢
145
+ * (杀毒软件扫描、磁盘繁忙),直接判死会让本可用的解释器被跳过。
139
146
  */
140
147
  function pythonVersion(exe) {
141
- const out = tryRun(exe, ['-c', 'import sys;print("%d.%d"%sys.version_info[:2])']);
142
- if (!out) return null;
143
- const m = /^(\d+)\.(\d+)$/.exec(out);
144
- return m ? [Number(m[1]), Number(m[2])] : null;
148
+ const probe = () => {
149
+ const out = tryRun(exe, ['-c', 'import sys;print("%d.%d"%sys.version_info[:2])']);
150
+ if (!out) return null;
151
+ const m = /^(\d+)\.(\d+)$/.exec(out);
152
+ return m ? [Number(m[1]), Number(m[2])] : null;
153
+ };
154
+
155
+ const first = probe();
156
+ if (first) return first;
157
+
158
+ // 一次失败不足以断定不可用,再试一次
159
+ return probe();
145
160
  }
146
161
 
147
162
  function versionAtLeast(ver, min) {
@@ -398,78 +413,38 @@ async function prepareRuntime() {
398
413
  }
399
414
 
400
415
  /* ------------------------------------------------------------------ *
401
- * 启动 MCP Server
416
+ * 启动(代理模式)
402
417
  * ------------------------------------------------------------------ */
403
418
 
404
419
  /**
405
- * 启动 MCP Server 并保持管道转发。
420
+ * 以代理模式启动:立即完成 MCP 握手,Python 运行时在后台准备。
406
421
  *
407
- * 返回 Promise<{code, fastExit, hadOutput}>:
408
- * - fastExit:进程在 15 秒内就退出(通常意味着环境损坏 / 依赖缺失)
409
- * - hadOutput:退出前是否已在 stdout 上产生过 MCP 消息
422
+ * 这样首次使用(需安装约 90 个 Python 依赖,实测 40~70 秒)也不会
423
+ * 触发客户端的 initialize 超时(通常 30 秒)。
410
424
  *
411
- * 只有「快速退出且没有任何输出」才值得自动修复重试,否则视为正常结束。
425
+ * 具体协议处理见 lib/proxy.js。
412
426
  */
413
- function startServer(venvPython) {
414
- const env = {
415
- ...process.env,
416
- PYTHONUTF8: '1',
417
- PYTHONIOENCODING: 'utf-8',
418
- };
419
-
420
- const src = vendoredSrcOrNull();
421
- if (src) {
422
- env.PYTHONPATH = env.PYTHONPATH ? src + path.delimiter + env.PYTHONPATH : src;
423
- }
424
-
425
- log('启动 MCP Server…');
426
- const startedAt = Date.now();
427
- let hadOutput = false;
428
-
429
- const child = spawn(venvPython, ['-m', ENTRY_MODULE], {
430
- stdio: ['pipe', 'pipe', 'pipe'],
431
- env,
432
- windowsHide: true,
427
+ function startWithProxy() {
428
+ const { createProxy } = require('./lib/proxy');
429
+ createProxy({
430
+ cwd: CONNECTOR_DIR,
431
+ version: pkgVersion(),
432
+ childArgs: ['-m', ENTRY_MODULE],
433
+ log,
434
+ ensureRuntime: async () => {
435
+ // 每次都要重新探测:用户可能刚装了 Python 或修好了环境
436
+ const py = await prepareRuntime();
437
+ return py;
438
+ },
433
439
  });
440
+ }
434
441
 
435
- // 双向转发:stdout 是 MCP 协议通道,必须原样透传
436
- child.stdout.on('data', (chunk) => {
437
- hadOutput = true;
438
- process.stdout.write(chunk);
439
- });
440
- child.stderr.pipe(process.stderr);
441
-
442
- process.stdin.pipe(child.stdin);
443
-
444
- // 客户端关闭 stdin 时,优雅关闭子进程
445
- process.stdin.on('end', () => {
446
- try {
447
- child.stdin.end();
448
- } catch {
449
- /* 子进程可能已退出 */
450
- }
451
- });
452
-
453
- // 管道错误不应导致启动器崩溃
454
- child.stdin.on('error', () => {});
455
- child.stdout.on('error', () => {});
456
- process.stdout.on('error', () => {});
457
-
458
- return new Promise((resolve) => {
459
- child.on('error', (e) => {
460
- log('无法启动 MCP Server:' + e.message);
461
- resolve({ code: 1, fastExit: true, hadOutput: false });
462
- });
463
-
464
- child.on('close', (code, signal) => {
465
- if (signal) log('MCP Server 被信号终止:' + signal);
466
- resolve({
467
- code: code == null ? 1 : code,
468
- fastExit: Date.now() - startedAt < 15000,
469
- hadOutput,
470
- });
471
- });
472
- });
442
+ function pkgVersion() {
443
+ try {
444
+ return JSON.parse(fs.readFileSync(path.join(CONNECTOR_DIR, 'package.json'), 'utf8')).version;
445
+ } catch {
446
+ return '0.0.0';
447
+ }
473
448
  }
474
449
 
475
450
  /* ------------------------------------------------------------------ *
@@ -535,9 +510,8 @@ async function main() {
535
510
  return;
536
511
  }
537
512
 
538
- const venvPython = await prepareRuntime();
539
- const result = await startServer(venvPython);
540
- process.exit(result.code);
513
+ // 正常启动:走代理模式,先握手再后台准备运行时
514
+ startWithProxy();
541
515
  }
542
516
 
543
517
  main().catch(fatal);