@atlisp/mcp 1.0.5 → 1.0.7

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
@@ -11,6 +11,8 @@ MCP (Model Context Protocol) Server for @lisp on CAD - 为 CAD 环境提供 @lis
11
11
  - 获取 CAD 平台信息
12
12
  - 通过 MCP 协议提供标准化接口
13
13
  - 管理 @lisp 包(列出、搜索、安装)
14
+ - 自动检测 CAD 状态(忙碌/空闲)
15
+ - CAD 无文档时自动处理
14
16
 
15
17
 
16
18
  ## 安装
@@ -121,6 +123,9 @@ atlisp-mcp
121
123
  | `TRANSPORT` | `http` | 传输模式:`http` 或 `stdio` |
122
124
  | `PORT` | `8110` | HTTP 模式监听端口 |
123
125
  | `HOST` | `0.0.0.0` | HTTP 模式监听地址 |
126
+ | `MESSAGE_TIMEOUT` | `60000` | 消息超时(毫秒) |
127
+ | `BUSY_RETRIES` | `10` | CAD 忙碌时重试次数 |
128
+ | `BUSY_DELAY` | `500` | CAD 忙碌重试间隔(毫秒) |
124
129
 
125
130
  ## API
126
131
 
@@ -144,7 +149,8 @@ HTTP 模式:`POST http://localhost:8110/mcp`
144
149
  | 工具名称 | 描述 | 参数 |
145
150
  |---------|------|------|
146
151
  | `connect_cad` | 连接到 CAD | 无 |
147
- | `eval_lisp` | 在 CAD 中执行 AutoLISP 代码 | `code`: 字符串 |
152
+ | `eval_lisp` | 在 CAD 中执行 AutoLISP 代码(不返回结果) | `code`: 字符串 |
153
+ | `eval_lisp_with_result` | 在 CAD 中执行 AutoLISP 代码并返回结果 | `code`: 字符串 |
148
154
  | `get_cad_info` | 获取当前 CAD 信息 | 无 |
149
155
  | `list_packages` | 列出已安装的 @lisp 包 | 无 |
150
156
  | `search_packages` | 搜索 @lisp 包 | `query`: 搜索关键词 |
@@ -153,6 +159,7 @@ HTTP 模式:`POST http://localhost:8110/mcp`
153
159
  | `get_file_extensions` | 获取指定平台的文件扩展名 | `platform`: 平台名称 |
154
160
  | `get_install_code` | 获取 @lisp 安装代码 | 无 |
155
161
  | `at_command` | 执行 @lisp 命令 | `command`: 命令 |
162
+ | `new_document` | 在 CAD 中新建空白文档 | 无 |
156
163
  | `install_atlisp` | 在 CAD 中安装 @lisp | 无 |
157
164
 
158
165
  ## 支持的平台
@@ -168,6 +175,9 @@ HTTP 模式:`POST http://localhost:8110/mcp`
168
175
  2. .NET 代码通过 COM 互操作连接 CAD 应用程序
169
176
  3. 支持获取活动 CAD 实例或启动新实例
170
177
  4. 通过 `SendCommand` 方法向 CAD 发送 LISP 代码
178
+ 5. 执行命令前自动检测 CAD 状态(IsQuiescent)
179
+ 6. CAD 忙碌时自动等待(最多 10 次重试)
180
+ 7. CAD 无打开文档时返回错误
171
181
 
172
182
  ## 示例
173
183
 
@@ -195,6 +205,14 @@ curl -X POST http://localhost:8110/mcp \
195
205
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_packages","arguments":{"query":"base"}}}'
196
206
  ```
197
207
 
208
+ ### 新建文档
209
+
210
+ ```bash
211
+ curl -X POST http://localhost:8110/mcp \
212
+ -H "Content-Type: application/json" \
213
+ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"new_document","arguments":{}}}'
214
+ ```
215
+
198
216
  ## 注意事项
199
217
 
200
218
  - 仅支持 Windows 平台(依赖 COM 互操作)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cad-worker.js CHANGED
@@ -2,6 +2,9 @@
2
2
  import edge from 'edge-js';
3
3
  import process from 'process';
4
4
 
5
+ const BUSY_RETRIES = parseInt(process.env.BUSY_RETRIES || '10', 10);
6
+ const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
7
+
5
8
  const cadConnect = edge.func(function() {/*
6
9
  async (input) => {
7
10
  // Try existing CAD instances
@@ -104,17 +107,6 @@ const cadSend = edge.func(function() {/*
104
107
  dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
105
108
  if (cad != null) {
106
109
  dynamic doc = cad.ActiveDocument;
107
- if (doc == null || doc.Name == "") {
108
- // Try default template path
109
- string templatePath = @"C:\Users\Public\Documents\AutoCAD 2024\Sample\basics.dwg";
110
- if (System.IO.File.Exists(templatePath)) {
111
- cad.Documents.Open(templatePath, false);
112
- } else {
113
- cad.Documents.Add("");
114
- }
115
- System.Threading.Thread.Sleep(2000);
116
- }
117
- doc = cad.ActiveDocument;
118
110
  if (doc != null) {
119
111
  doc.SendCommand(code);
120
112
  return new { success = true };
@@ -123,7 +115,49 @@ const cadSend = edge.func(function() {/*
123
115
  } catch {}
124
116
  return new { success = false };
125
117
  }
126
- */});
118
+ */});
119
+
120
+ const cadSendWithResult = edge.func(function() {/*
121
+ async (input) => {
122
+ dynamic d = input;
123
+ string code = d.code.ToString();
124
+ string platform = d.platform.ToString();
125
+ string progId = platform + ".Application";
126
+
127
+ string tempDir = System.IO.Path.GetTempPath();
128
+ string tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
129
+
130
+ // Use print to write result directly to file
131
+ string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(print " + code + " f)(close f))";
132
+
133
+ try {
134
+ dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
135
+ if (cad != null) {
136
+ dynamic doc = cad.ActiveDocument;
137
+ if (doc != null) {
138
+ doc.SendCommand(lispCode + "\n");
139
+ System.Threading.Thread.Sleep(500);
140
+
141
+ if (System.IO.File.Exists(tempFile)) {
142
+ string result = System.IO.File.ReadAllText(tempFile).Trim();
143
+ try { System.IO.File.Delete(tempFile); } catch {}
144
+ if (!string.IsNullOrEmpty(result)) {
145
+ return new { success = true, result = result };
146
+ }
147
+ // Try evaluate result directly if file is empty
148
+ return new { success = true, result = "" };
149
+ }
150
+ return new { success = true, result = "" };
151
+ }
152
+ }
153
+ } catch (Exception ex) {
154
+ try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
155
+ return new { success = false, error = ex.Message };
156
+ }
157
+ try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
158
+ return new { success = false };
159
+ }
160
+ */});
127
161
 
128
162
  process.stdin.setEncoding('utf8');
129
163
 
@@ -144,22 +178,73 @@ process.stdin.on('data', (data) => {
144
178
  }
145
179
  });
146
180
 
147
- const cadHasDoc = edge.func(function() {/*
181
+ const cadIsBusy = edge.func(function() {/*
148
182
  async (input) => {
149
183
  string platform = input.ToString();
150
184
  string progId = platform + ".Application";
151
185
  try {
152
186
  dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
153
187
  if (cad != null) {
154
- dynamic doc = cad.ActiveDocument;
155
- if (doc != null && doc.Name != "") {
156
- return new { hasDoc = true, name = doc.Name };
188
+ bool isBusy = false;
189
+ if (platform == "ZWCAD") {
190
+ isBusy = !(cad.GetZcadState().IsQuiescent);
191
+ } else {
192
+ isBusy = !(cad.GetAcadState().IsQuiescent);
157
193
  }
194
+ return new { isBusy = isBusy };
158
195
  }
159
196
  } catch {}
160
- return new { hasDoc = false };
197
+ return new { isBusy = false };
161
198
  }
162
- */});
199
+ */});
200
+
201
+ const cadHasDoc = edge.func(function() {/*
202
+ async (input) => {
203
+ string platform = input.ToString();
204
+ string progId = platform + ".Application";
205
+ try {
206
+ dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
207
+ if (cad != null) {
208
+ int docCount = cad.Documents.Count;
209
+ return new { hasDoc = docCount > 0, docCount = docCount };
210
+ }
211
+ } catch {}
212
+ return new { hasDoc = false, docCount = 0 };
213
+ }
214
+ */});
215
+
216
+ const cadNewDoc = edge.func(function() {/*
217
+ async (input) => {
218
+ string platform = input.ToString();
219
+ string progId = platform + ".Application";
220
+ try {
221
+ dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
222
+ if (cad != null) {
223
+ cad.Documents.Add();
224
+ return new { success = true };
225
+ }
226
+ } catch (Exception ex) {
227
+ return new { success = false, error = ex.Message };
228
+ }
229
+ return new { success = false, error = "CAD not found" };
230
+ }
231
+ */});
232
+
233
+ async function waitForIdle(platform) {
234
+ let retries = BUSY_RETRIES;
235
+ while (retries > 0) {
236
+ const checkResult = await new Promise((resolve, reject) => {
237
+ cadIsBusy(platform || 'AutoCAD', (e, r) => {
238
+ if (e) reject(e);
239
+ else resolve(r);
240
+ });
241
+ });
242
+ if (!checkResult.isBusy) return true;
243
+ retries--;
244
+ if (retries > 0) await new Promise(resolve => setTimeout(resolve, BUSY_DELAY));
245
+ }
246
+ return false;
247
+ }
163
248
 
164
249
  async function handleMessage(msg) {
165
250
  if (msg.type === 'connect') {
@@ -178,20 +263,26 @@ async function handleMessage(msg) {
178
263
  });
179
264
  });
180
265
  }
181
- if (msg.type === 'send') {
182
- const checkResult = await new Promise((resolve, reject) => {
183
- cadHasDoc(msg.platform, (e, r) => {
266
+ if (msg.type === 'newdoc') {
267
+ return new Promise((resolve, reject) => {
268
+ cadNewDoc(msg.platform || 'AutoCAD', (e, r) => {
184
269
  if (e) reject(e);
185
270
  else resolve(r);
186
271
  });
187
272
  });
188
- if (!checkResult.hasDoc) {
189
- const newDoc = await new Promise((resolve, reject) => {
190
- cadSend({ code: "\n", platform: msg.platform }, (e, r) => {
191
- if (e) reject(e);
192
- else resolve(r);
193
- });
273
+ }
274
+ if (msg.type === 'send') {
275
+ const docCheck = await new Promise((resolve, reject) => {
276
+ cadHasDoc(msg.platform || 'AutoCAD', (e, r) => {
277
+ if (e) reject(e);
278
+ else resolve(r);
194
279
  });
280
+ });
281
+ if (!docCheck.hasDoc) {
282
+ return { success: false, error: 'No open document' };
283
+ }
284
+ if (!(await waitForIdle(msg.platform))) {
285
+ return { success: false, error: 'CAD is busy, timeout' };
195
286
  }
196
287
  return new Promise((resolve, reject) => {
197
288
  cadSend({ code: msg.code, platform: msg.platform }, (e, r) => {
@@ -200,5 +291,33 @@ async function handleMessage(msg) {
200
291
  });
201
292
  });
202
293
  }
294
+ if (msg.type === 'sendResult') {
295
+ const docCheck = await new Promise((resolve, reject) => {
296
+ cadHasDoc(msg.platform || 'AutoCAD', (e, r) => {
297
+ if (e) reject(e);
298
+ else resolve(r);
299
+ });
300
+ });
301
+ if (!docCheck.hasDoc) {
302
+ return { success: false, error: 'No open document' };
303
+ }
304
+ if (!(await waitForIdle(msg.platform))) {
305
+ return { success: false, error: 'CAD is busy, timeout' };
306
+ }
307
+ return new Promise((resolve, reject) => {
308
+ cadSendWithResult({ code: msg.code, platform: msg.platform }, (e, r) => {
309
+ if (e) reject(e);
310
+ else resolve(r);
311
+ });
312
+ });
313
+ }
314
+ if (msg.type === 'isBusy') {
315
+ return new Promise((resolve, reject) => {
316
+ cadIsBusy(msg.platform || 'AutoCAD', (e, r) => {
317
+ if (e) reject(e);
318
+ else resolve(r);
319
+ });
320
+ });
321
+ }
203
322
  return { error: 'unknown message type' };
204
323
  }
package/src/cad.js CHANGED
@@ -11,6 +11,8 @@ const FILE_EXTENSIONS = {
11
11
  'BricsCAD': ['.des']
12
12
  };
13
13
 
14
+ const MESSAGE_TIMEOUT = parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10);
15
+
14
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
17
  const workerPath = path.join(__dirname, 'cad-worker.js');
16
18
 
@@ -44,7 +46,7 @@ function sendMessage(msg) {
44
46
  responded = true;
45
47
  reject(new Error('Timeout waiting for worker'));
46
48
  }
47
- }, 60000);
49
+ }, MESSAGE_TIMEOUT);
48
50
 
49
51
  const handler = (data) => {
50
52
  try {
@@ -114,8 +116,51 @@ class CadConnection {
114
116
  }
115
117
  }
116
118
 
119
+ async sendCommandWithResult(code) {
120
+ if (!this.connected) throw new Error('未连接 CAD');
121
+ try {
122
+ const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform });
123
+ if (result && result.success) {
124
+ return result.result || '';
125
+ }
126
+ return null;
127
+ } catch (e) {
128
+ return null;
129
+ }
130
+ }
131
+
117
132
  getVersion() { return this.version; }
118
133
  getPlatform() { return this.product; }
134
+
135
+ async isBusy() {
136
+ if (!this.connected) return false;
137
+ try {
138
+ const result = await sendMessage({ type: 'isBusy', platform: _platform });
139
+ return result && result.isBusy;
140
+ } catch (e) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ async hasDoc() {
146
+ if (!this.connected) return { hasDoc: false, docCount: 0 };
147
+ try {
148
+ const result = await sendMessage({ type: 'hasdoc', platform: _platform });
149
+ return result || { hasDoc: false, docCount: 0 };
150
+ } catch (e) {
151
+ return { hasDoc: false, docCount: 0 };
152
+ }
153
+ }
154
+
155
+ async newDoc() {
156
+ if (!this.connected) throw new Error('未连接 CAD');
157
+ try {
158
+ const result = await sendMessage({ type: 'newdoc', platform: _platform });
159
+ return result && result.success;
160
+ } catch (e) {
161
+ return false;
162
+ }
163
+ }
119
164
 
120
165
  async getInfo() {
121
166
  if (!this.connected) return 'CAD 未连接';
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import express from 'express';
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
4
  import {
6
5
  ListToolsRequestSchema,
7
6
  CallToolRequestSchema,
@@ -48,7 +47,16 @@ const tools = [
48
47
  },
49
48
  {
50
49
  name: 'eval_lisp',
51
- description: '在 CAD 中执行 AutoLISP 代码',
50
+ description: '在 CAD 中执行 AutoLISP 代码(不返回结果)',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
54
+ required: ['code']
55
+ }
56
+ },
57
+ {
58
+ name: 'eval_lisp_with_result',
59
+ description: '在 CAD 中执行 AutoLISP 代码并返回结果',
52
60
  inputSchema: {
53
61
  type: 'object',
54
62
  properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
@@ -111,6 +119,11 @@ const tools = [
111
119
  required: ['command']
112
120
  }
113
121
  },
122
+ {
123
+ name: 'new_document',
124
+ description: '在 CAD 中新建空白文档',
125
+ inputSchema: { type: 'object', properties: {} }
126
+ },
114
127
  {
115
128
  name: 'install_atlisp',
116
129
  description: '在 CAD 中安装 @lisp',
@@ -140,6 +153,15 @@ async function handleToolCall(name, args) {
140
153
  await cad.sendCommand(args.code + '\n');
141
154
  return { content: [{ type: 'text', text: `已发送命令: ${args.code}` }] };
142
155
 
156
+ case 'eval_lisp_with_result':
157
+ if (!cad.connected) { await cad.connect(); }
158
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
159
+ const result = await cad.sendCommandWithResult(args.code);
160
+ if (result !== null) {
161
+ return { content: [{ type: 'text', text: result }] };
162
+ }
163
+ return { content: [{ type: 'text', text: '执行失败或无返回值' }], isError: true };
164
+
143
165
  case 'get_cad_info':
144
166
  if (!cad.connected) { await cad.connect(); }
145
167
  if (!cad.connected) return { content: [{ type: 'text', text: 'CAD 未连接' }] };
@@ -178,6 +200,15 @@ async function handleToolCall(name, args) {
178
200
  await cad.sendCommand(`@${args.command}\n`);
179
201
  return { content: [{ type: 'text', text: `已发送命令: @${args.command}` }] };
180
202
 
203
+ case 'new_document':
204
+ if (!cad.connected) { await cad.connect(); }
205
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
206
+ const docResult = await cad.newDoc();
207
+ if (docResult) {
208
+ return { content: [{ type: 'text', text: '已在 CAD 中新建空白文档' }] };
209
+ }
210
+ return { content: [{ type: 'text', text: '新建文档失败' }], isError: true };
211
+
181
212
  case 'install_atlisp':
182
213
  if (!cad.connected) { await cad.connect(); }
183
214
  if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
@@ -191,7 +222,7 @@ async function handleToolCall(name, args) {
191
222
  }
192
223
 
193
224
  const mcpServer = new Server(
194
- { name: 'atlisp-mcp-server', version: '1.0.0' },
225
+ { name: 'atlisp-mcp-server', version: '1.0.7' },
195
226
  { capabilities: { tools: {} } }
196
227
  );
197
228
 
@@ -251,7 +282,7 @@ if (TRANSPORT === 'stdio') {
251
282
  result: {
252
283
  protocolVersion: '2024-11-05',
253
284
  capabilities: { tools: {} },
254
- serverInfo: { name: 'atlisp-mcp-server', version: '1.0.0' }
285
+ serverInfo: { name: 'atlisp-mcp-server', version: '1.0.7' }
255
286
  }
256
287
  });
257
288
  }