@atlisp/mcp 1.0.4 → 1.0.6
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 +25 -24
- package/package.json +1 -1
- package/src/cad-worker.js +151 -12
- package/src/cad.js +43 -0
- package/src/index.js +33 -1
package/README.md
CHANGED
|
@@ -11,12 +11,22 @@ 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
|
## 安装
|
|
17
19
|
|
|
20
|
+
### Windows (PowerShell)
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
iwr -useb https://atlisp.cn/install.ps1 | iex
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### macOS / Linux
|
|
27
|
+
|
|
18
28
|
```bash
|
|
19
|
-
npm i -g @atlisp/mcp
|
|
29
|
+
npm i -g @atlisp/mcp
|
|
20
30
|
```
|
|
21
31
|
|
|
22
32
|
## 使用方法
|
|
@@ -131,28 +141,13 @@ curl http://localhost:8110/health
|
|
|
131
141
|
|
|
132
142
|
HTTP 模式:`POST http://localhost:8110/mcp`
|
|
133
143
|
|
|
134
|
-
#### 列出工具
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
curl -X POST http://localhost:8110/mcp \
|
|
138
|
-
-H "Content-Type: application/json" \
|
|
139
|
-
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### 调用工具
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
curl -X POST http://localhost:8110/mcp \
|
|
146
|
-
-H "Content-Type: application/json" \
|
|
147
|
-
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"工具名","arguments":{参数}}}'
|
|
148
|
-
```
|
|
149
|
-
|
|
150
144
|
## 可用工具
|
|
151
145
|
|
|
152
146
|
| 工具名称 | 描述 | 参数 |
|
|
153
147
|
|---------|------|------|
|
|
154
148
|
| `connect_cad` | 连接到 CAD | 无 |
|
|
155
|
-
| `eval_lisp` | 在 CAD 中执行 AutoLISP
|
|
149
|
+
| `eval_lisp` | 在 CAD 中执行 AutoLISP 代码(不返回结果) | `code`: 字符串 |
|
|
150
|
+
| `eval_lisp_with_result` | 在 CAD 中执行 AutoLISP 代码并返回结果 | `code`: 字符串 |
|
|
156
151
|
| `get_cad_info` | 获取当前 CAD 信息 | 无 |
|
|
157
152
|
| `list_packages` | 列出已安装的 @lisp 包 | 无 |
|
|
158
153
|
| `search_packages` | 搜索 @lisp 包 | `query`: 搜索关键词 |
|
|
@@ -161,6 +156,7 @@ curl -X POST http://localhost:8110/mcp \
|
|
|
161
156
|
| `get_file_extensions` | 获取指定平台的文件扩展名 | `platform`: 平台名称 |
|
|
162
157
|
| `get_install_code` | 获取 @lisp 安装代码 | 无 |
|
|
163
158
|
| `at_command` | 执行 @lisp 命令 | `command`: 命令 |
|
|
159
|
+
| `new_document` | 在 CAD 中新建空白文档 | 无 |
|
|
164
160
|
| `install_atlisp` | 在 CAD 中安装 @lisp | 无 |
|
|
165
161
|
|
|
166
162
|
## 支持的平台
|
|
@@ -170,18 +166,15 @@ curl -X POST http://localhost:8110/mcp \
|
|
|
170
166
|
- **GStarCAD** (文件扩展名: `.des`)
|
|
171
167
|
- **BricsCAD** (文件扩展名: `.des`)
|
|
172
168
|
|
|
173
|
-
## 技术栈
|
|
174
|
-
|
|
175
|
-
- **Runtime**: Node.js >= 18
|
|
176
|
-
- **MCP SDK**: `@modelcontextprotocol/sdk`
|
|
177
|
-
- **CAD 连接**: `edge-js` (通过 .NET COM 互操作)
|
|
178
|
-
|
|
179
169
|
## 工作原理
|
|
180
170
|
|
|
181
171
|
1. MCP Server 通过 `edge-js` 调用 .NET 代码
|
|
182
172
|
2. .NET 代码通过 COM 互操作连接 CAD 应用程序
|
|
183
173
|
3. 支持获取活动 CAD 实例或启动新实例
|
|
184
174
|
4. 通过 `SendCommand` 方法向 CAD 发送 LISP 代码
|
|
175
|
+
5. 执行命令前自动检测 CAD 状态(IsQuiescent)
|
|
176
|
+
6. CAD 忙碌时自动等待(最多 10 次重试)
|
|
177
|
+
7. CAD 无打开文档时返回错误
|
|
185
178
|
|
|
186
179
|
## 示例
|
|
187
180
|
|
|
@@ -209,6 +202,14 @@ curl -X POST http://localhost:8110/mcp \
|
|
|
209
202
|
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_packages","arguments":{"query":"base"}}}'
|
|
210
203
|
```
|
|
211
204
|
|
|
205
|
+
### 新建文档
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
curl -X POST http://localhost:8110/mcp \
|
|
209
|
+
-H "Content-Type: application/json" \
|
|
210
|
+
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"new_document","arguments":{}}}'
|
|
211
|
+
```
|
|
212
|
+
|
|
212
213
|
## 注意事项
|
|
213
214
|
|
|
214
215
|
- 仅支持 Windows 平台(依赖 COM 互操作)
|
package/package.json
CHANGED
package/src/cad-worker.js
CHANGED
|
@@ -105,7 +105,6 @@ const cadSend = edge.func(function() {/*
|
|
|
105
105
|
if (cad != null) {
|
|
106
106
|
dynamic doc = cad.ActiveDocument;
|
|
107
107
|
if (doc == null || doc.Name == "") {
|
|
108
|
-
// Try default template path
|
|
109
108
|
string templatePath = @"C:\Users\Public\Documents\AutoCAD 2024\Sample\basics.dwg";
|
|
110
109
|
if (System.IO.File.Exists(templatePath)) {
|
|
111
110
|
cad.Documents.Open(templatePath, false);
|
|
@@ -125,6 +124,53 @@ const cadSend = edge.func(function() {/*
|
|
|
125
124
|
}
|
|
126
125
|
*/});
|
|
127
126
|
|
|
127
|
+
const cadSendWithResult = edge.func(function() {/*
|
|
128
|
+
async (input) => {
|
|
129
|
+
dynamic d = input;
|
|
130
|
+
string code = d.code.ToString();
|
|
131
|
+
string platform = d.platform.ToString();
|
|
132
|
+
string progId = platform + ".Application";
|
|
133
|
+
|
|
134
|
+
string tempDir = System.IO.Path.GetTempPath();
|
|
135
|
+
string tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
|
|
136
|
+
|
|
137
|
+
string lispCode = code + "\n(setq _atlisp_result (princ-to-string " + code + "))(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(write-line _atlisp_result f)(close f))\n";
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
141
|
+
if (cad != null) {
|
|
142
|
+
dynamic doc = cad.ActiveDocument;
|
|
143
|
+
if (doc == null || doc.Name == "") {
|
|
144
|
+
string templatePath = @"C:\Users\Public\Documents\AutoCAD 2024\Sample\basics.dwg";
|
|
145
|
+
if (System.IO.File.Exists(templatePath)) {
|
|
146
|
+
cad.Documents.Open(templatePath, false);
|
|
147
|
+
} else {
|
|
148
|
+
cad.Documents.Add("");
|
|
149
|
+
}
|
|
150
|
+
System.Threading.Thread.Sleep(2000);
|
|
151
|
+
}
|
|
152
|
+
doc = cad.ActiveDocument;
|
|
153
|
+
if (doc != null) {
|
|
154
|
+
doc.SendCommand(lispCode + "\n");
|
|
155
|
+
System.Threading.Thread.Sleep(500);
|
|
156
|
+
|
|
157
|
+
if (System.IO.File.Exists(tempFile)) {
|
|
158
|
+
string result = System.IO.File.ReadAllText(tempFile).Trim();
|
|
159
|
+
try { System.IO.File.Delete(tempFile); } catch {}
|
|
160
|
+
return new { success = true, result = result };
|
|
161
|
+
}
|
|
162
|
+
return new { success = true, result = "" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (Exception ex) {
|
|
166
|
+
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
167
|
+
return new { success = false, error = ex.Message };
|
|
168
|
+
}
|
|
169
|
+
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
170
|
+
return new { success = false };
|
|
171
|
+
}
|
|
172
|
+
*/});
|
|
173
|
+
|
|
128
174
|
process.stdin.setEncoding('utf8');
|
|
129
175
|
|
|
130
176
|
process.stdin.on('data', (data) => {
|
|
@@ -144,22 +190,57 @@ process.stdin.on('data', (data) => {
|
|
|
144
190
|
}
|
|
145
191
|
});
|
|
146
192
|
|
|
147
|
-
const
|
|
193
|
+
const cadIsBusy = edge.func(function() {/*
|
|
148
194
|
async (input) => {
|
|
149
195
|
string platform = input.ToString();
|
|
150
196
|
string progId = platform + ".Application";
|
|
151
197
|
try {
|
|
152
198
|
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
153
199
|
if (cad != null) {
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
200
|
+
bool isBusy = false;
|
|
201
|
+
if (platform == "ZWCAD") {
|
|
202
|
+
isBusy = !(cad.GetZcadState().IsQuiescent);
|
|
203
|
+
} else {
|
|
204
|
+
isBusy = !(cad.GetAcadState().IsQuiescent);
|
|
157
205
|
}
|
|
206
|
+
return new { isBusy = isBusy };
|
|
158
207
|
}
|
|
159
208
|
} catch {}
|
|
160
|
-
return new {
|
|
209
|
+
return new { isBusy = false };
|
|
161
210
|
}
|
|
162
|
-
*/});
|
|
211
|
+
*/});
|
|
212
|
+
|
|
213
|
+
const cadHasDoc = edge.func(function() {/*
|
|
214
|
+
async (input) => {
|
|
215
|
+
string platform = input.ToString();
|
|
216
|
+
string progId = platform + ".Application";
|
|
217
|
+
try {
|
|
218
|
+
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
219
|
+
if (cad != null) {
|
|
220
|
+
int docCount = cad.Documents.Count;
|
|
221
|
+
return new { hasDoc = docCount > 0, docCount = docCount };
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
return new { hasDoc = false, docCount = 0 };
|
|
225
|
+
}
|
|
226
|
+
*/});
|
|
227
|
+
|
|
228
|
+
const cadNewDoc = edge.func(function() {/*
|
|
229
|
+
async (input) => {
|
|
230
|
+
string platform = input.ToString();
|
|
231
|
+
string progId = platform + ".Application";
|
|
232
|
+
try {
|
|
233
|
+
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
234
|
+
if (cad != null) {
|
|
235
|
+
cad.Documents.Add();
|
|
236
|
+
return new { success = true };
|
|
237
|
+
}
|
|
238
|
+
} catch (Exception ex) {
|
|
239
|
+
return new { success = false, error = ex.Message };
|
|
240
|
+
}
|
|
241
|
+
return new { success = false, error = "CAD not found" };
|
|
242
|
+
}
|
|
243
|
+
*/});
|
|
163
244
|
|
|
164
245
|
async function handleMessage(msg) {
|
|
165
246
|
if (msg.type === 'connect') {
|
|
@@ -178,20 +259,38 @@ async function handleMessage(msg) {
|
|
|
178
259
|
});
|
|
179
260
|
});
|
|
180
261
|
}
|
|
262
|
+
if (msg.type === 'newdoc') {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
cadNewDoc(msg.platform || 'AutoCAD', (e, r) => {
|
|
265
|
+
if (e) reject(e);
|
|
266
|
+
else resolve(r);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
181
270
|
if (msg.type === 'send') {
|
|
182
|
-
const
|
|
183
|
-
cadHasDoc(msg.platform, (e, r) => {
|
|
271
|
+
const docCheck = await new Promise((resolve, reject) => {
|
|
272
|
+
cadHasDoc(msg.platform || 'AutoCAD', (e, r) => {
|
|
184
273
|
if (e) reject(e);
|
|
185
274
|
else resolve(r);
|
|
186
275
|
});
|
|
187
276
|
});
|
|
188
|
-
if (!
|
|
189
|
-
|
|
190
|
-
|
|
277
|
+
if (!docCheck.hasDoc) {
|
|
278
|
+
return { success: false, error: 'No open document' };
|
|
279
|
+
}
|
|
280
|
+
let retries = 10;
|
|
281
|
+
while (retries > 0) {
|
|
282
|
+
const checkResult = await new Promise((resolve, reject) => {
|
|
283
|
+
cadIsBusy(msg.platform || 'AutoCAD', (e, r) => {
|
|
191
284
|
if (e) reject(e);
|
|
192
285
|
else resolve(r);
|
|
193
286
|
});
|
|
194
287
|
});
|
|
288
|
+
if (!checkResult.isBusy) break;
|
|
289
|
+
retries--;
|
|
290
|
+
if (retries > 0) await new Promise(resolve => setTimeout(resolve, 500));
|
|
291
|
+
}
|
|
292
|
+
if (retries === 0) {
|
|
293
|
+
return { success: false, error: 'CAD is busy, timeout' };
|
|
195
294
|
}
|
|
196
295
|
return new Promise((resolve, reject) => {
|
|
197
296
|
cadSend({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
@@ -200,5 +299,45 @@ async function handleMessage(msg) {
|
|
|
200
299
|
});
|
|
201
300
|
});
|
|
202
301
|
}
|
|
302
|
+
if (msg.type === 'sendResult') {
|
|
303
|
+
const docCheck = await new Promise((resolve, reject) => {
|
|
304
|
+
cadHasDoc(msg.platform || 'AutoCAD', (e, r) => {
|
|
305
|
+
if (e) reject(e);
|
|
306
|
+
else resolve(r);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
if (!docCheck.hasDoc) {
|
|
310
|
+
return { success: false, error: 'No open document' };
|
|
311
|
+
}
|
|
312
|
+
let retries = 10;
|
|
313
|
+
while (retries > 0) {
|
|
314
|
+
const checkResult = await new Promise((resolve, reject) => {
|
|
315
|
+
cadIsBusy(msg.platform || 'AutoCAD', (e, r) => {
|
|
316
|
+
if (e) reject(e);
|
|
317
|
+
else resolve(r);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
if (!checkResult.isBusy) break;
|
|
321
|
+
retries--;
|
|
322
|
+
if (retries > 0) await new Promise(resolve => setTimeout(resolve, 500));
|
|
323
|
+
}
|
|
324
|
+
if (retries === 0) {
|
|
325
|
+
return { success: false, error: 'CAD is busy, timeout' };
|
|
326
|
+
}
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
cadSendWithResult({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
329
|
+
if (e) reject(e);
|
|
330
|
+
else resolve(r);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (msg.type === 'isBusy') {
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
cadIsBusy(msg.platform || 'AutoCAD', (e, r) => {
|
|
337
|
+
if (e) reject(e);
|
|
338
|
+
else resolve(r);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
203
342
|
return { error: 'unknown message type' };
|
|
204
343
|
}
|
package/src/cad.js
CHANGED
|
@@ -114,8 +114,51 @@ class CadConnection {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
async sendCommandWithResult(code) {
|
|
118
|
+
if (!this.connected) throw new Error('未连接 CAD');
|
|
119
|
+
try {
|
|
120
|
+
const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform });
|
|
121
|
+
if (result && result.success) {
|
|
122
|
+
return result.result || '';
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
getVersion() { return this.version; }
|
|
118
131
|
getPlatform() { return this.product; }
|
|
132
|
+
|
|
133
|
+
async isBusy() {
|
|
134
|
+
if (!this.connected) return false;
|
|
135
|
+
try {
|
|
136
|
+
const result = await sendMessage({ type: 'isBusy', platform: _platform });
|
|
137
|
+
return result && result.isBusy;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async hasDoc() {
|
|
144
|
+
if (!this.connected) return { hasDoc: false, docCount: 0 };
|
|
145
|
+
try {
|
|
146
|
+
const result = await sendMessage({ type: 'hasdoc', platform: _platform });
|
|
147
|
+
return result || { hasDoc: false, docCount: 0 };
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return { hasDoc: false, docCount: 0 };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async newDoc() {
|
|
154
|
+
if (!this.connected) throw new Error('未连接 CAD');
|
|
155
|
+
try {
|
|
156
|
+
const result = await sendMessage({ type: 'newdoc', platform: _platform });
|
|
157
|
+
return result && result.success;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
119
162
|
|
|
120
163
|
async getInfo() {
|
|
121
164
|
if (!this.connected) return 'CAD 未连接';
|
package/src/index.js
CHANGED
|
@@ -48,7 +48,16 @@ const tools = [
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: 'eval_lisp',
|
|
51
|
-
description: '在 CAD 中执行 AutoLISP
|
|
51
|
+
description: '在 CAD 中执行 AutoLISP 代码(不返回结果)',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
|
|
55
|
+
required: ['code']
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'eval_lisp_with_result',
|
|
60
|
+
description: '在 CAD 中执行 AutoLISP 代码并返回结果',
|
|
52
61
|
inputSchema: {
|
|
53
62
|
type: 'object',
|
|
54
63
|
properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
|
|
@@ -111,6 +120,11 @@ const tools = [
|
|
|
111
120
|
required: ['command']
|
|
112
121
|
}
|
|
113
122
|
},
|
|
123
|
+
{
|
|
124
|
+
name: 'new_document',
|
|
125
|
+
description: '在 CAD 中新建空白文档',
|
|
126
|
+
inputSchema: { type: 'object', properties: {} }
|
|
127
|
+
},
|
|
114
128
|
{
|
|
115
129
|
name: 'install_atlisp',
|
|
116
130
|
description: '在 CAD 中安装 @lisp',
|
|
@@ -140,6 +154,15 @@ async function handleToolCall(name, args) {
|
|
|
140
154
|
await cad.sendCommand(args.code + '\n');
|
|
141
155
|
return { content: [{ type: 'text', text: `已发送命令: ${args.code}` }] };
|
|
142
156
|
|
|
157
|
+
case 'eval_lisp_with_result':
|
|
158
|
+
if (!cad.connected) { await cad.connect(); }
|
|
159
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
160
|
+
const result = await cad.sendCommandWithResult(args.code);
|
|
161
|
+
if (result !== null) {
|
|
162
|
+
return { content: [{ type: 'text', text: result }] };
|
|
163
|
+
}
|
|
164
|
+
return { content: [{ type: 'text', text: '执行失败或无返回值' }], isError: true };
|
|
165
|
+
|
|
143
166
|
case 'get_cad_info':
|
|
144
167
|
if (!cad.connected) { await cad.connect(); }
|
|
145
168
|
if (!cad.connected) return { content: [{ type: 'text', text: 'CAD 未连接' }] };
|
|
@@ -178,6 +201,15 @@ async function handleToolCall(name, args) {
|
|
|
178
201
|
await cad.sendCommand(`@${args.command}\n`);
|
|
179
202
|
return { content: [{ type: 'text', text: `已发送命令: @${args.command}` }] };
|
|
180
203
|
|
|
204
|
+
case 'new_document':
|
|
205
|
+
if (!cad.connected) { await cad.connect(); }
|
|
206
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
207
|
+
const docResult = await cad.newDoc();
|
|
208
|
+
if (docResult) {
|
|
209
|
+
return { content: [{ type: 'text', text: '已在 CAD 中新建空白文档' }] };
|
|
210
|
+
}
|
|
211
|
+
return { content: [{ type: 'text', text: '新建文档失败' }], isError: true };
|
|
212
|
+
|
|
181
213
|
case 'install_atlisp':
|
|
182
214
|
if (!cad.connected) { await cad.connect(); }
|
|
183
215
|
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|