@atlisp/mcp 1.0.12 → 1.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
package/src/atlisp-mcp.js CHANGED
@@ -5,6 +5,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
5
  import {
6
6
  ListToolsRequestSchema,
7
7
  CallToolRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ SubscribeRequestSchema,
11
+ UnsubscribeRequestSchema,
8
12
  } from '@modelcontextprotocol/sdk/types.js';
9
13
  import { cad } from './cad.js';
10
14
  import { CAD_PLATFORMS, FILE_EXTENSIONS, PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION } from './constants.js';
@@ -12,6 +16,8 @@ import { log } from './logger.js';
12
16
  import * as cadHandlers from './handlers/cad-handlers.js';
13
17
  import * as packageHandlers from './handlers/package-handlers.js';
14
18
  import * as functionHandlers from './handlers/function-handlers.js';
19
+ import { listResources, readResource } from './handlers/resource-handlers.js';
20
+ import * as subMgr from './subscription-manager.js';
15
21
  import { createSseResponseHandler } from './sse-utils.js';
16
22
 
17
23
  const PORT = process.env.PORT || 8110;
@@ -184,7 +190,7 @@ let server = null;
184
190
  export function createMcpServer() {
185
191
  server = new Server(
186
192
  { name: SERVER_NAME, version: SERVER_VERSION },
187
- { capabilities: { tools: {} } }
193
+ { capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } } }
188
194
  );
189
195
 
190
196
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -194,6 +200,42 @@ export function createMcpServer() {
194
200
  return await handleToolCall(name, args || {});
195
201
  });
196
202
 
203
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
204
+ resources: await listResources(subMgr.list())
205
+ }));
206
+
207
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
208
+ const { uri } = request.params;
209
+ try {
210
+ const data = await readResource(uri);
211
+ return {
212
+ contents: [{
213
+ uri,
214
+ mimeType: 'application/json',
215
+ text: JSON.stringify(data)
216
+ }]
217
+ };
218
+ } catch (e) {
219
+ return {
220
+ contents: [],
221
+ isError: true,
222
+ error: { code: -32603, message: e.message }
223
+ };
224
+ }
225
+ });
226
+
227
+ server.setRequestHandler(SubscribeRequestSchema, async (request) => {
228
+ const { uri } = request.params;
229
+ subMgr.subscribe(uri);
230
+ return { success: true };
231
+ });
232
+
233
+ server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
234
+ const { uri } = request.params;
235
+ subMgr.unsubscribe(uri);
236
+ return { success: true };
237
+ });
238
+
197
239
  return server;
198
240
  }
199
241
 
@@ -263,7 +305,7 @@ export async function startServer() {
263
305
  if (method === 'initialize') {
264
306
  send({ jsonrpc: '2.0', id, result: {
265
307
  protocolVersion: PROTOCOL_VERSION,
266
- capabilities: { tools: {} },
308
+ capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
267
309
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
268
310
  } }, true);
269
311
  return;
@@ -274,6 +316,49 @@ export async function startServer() {
274
316
  return;
275
317
  }
276
318
 
319
+ if (method === 'resources/list') {
320
+ const resources = await listResources(subMgr.list());
321
+ send({ jsonrpc: '2.0', id, result: { resources } }, true);
322
+ return;
323
+ }
324
+
325
+ if (method === 'resources/read') {
326
+ const uri = params?.uri;
327
+ if (!uri) {
328
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
329
+ return;
330
+ }
331
+ try {
332
+ const data = await readResource(uri);
333
+ send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
334
+ } catch (e) {
335
+ send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
336
+ }
337
+ return;
338
+ }
339
+
340
+ if (method === 'resources/subscribe') {
341
+ const uri = params?.uri;
342
+ if (!uri) {
343
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
344
+ return;
345
+ }
346
+ subMgr.subscribe(uri);
347
+ send({ jsonrpc: '2.0', id, result: { success: true } }, true);
348
+ return;
349
+ }
350
+
351
+ if (method === 'resources/unsubscribe') {
352
+ const uri = params?.uri;
353
+ if (!uri) {
354
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
355
+ return;
356
+ }
357
+ subMgr.unsubscribe(uri);
358
+ send({ jsonrpc: '2.0', id, result: { success: true } }, true);
359
+ return;
360
+ }
361
+
277
362
  if (method === 'tools/call') {
278
363
  const toolName = params?.name;
279
364
  if (!toolName) {
package/src/cad-worker.js CHANGED
@@ -79,16 +79,31 @@ const cadSendWithResult = edge.func(function() {/*
79
79
  string progId = platform + ".Application";
80
80
  string tempDir = System.IO.Path.GetTempPath();
81
81
  tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
82
- string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(print " + code + " f)(close f))";
82
+ string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(print " + code + " f)(close f)(princ))";
83
83
  dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
84
84
  if (cad != null) {
85
85
  dynamic doc = cad.ActiveDocument;
86
86
  if (doc != null) {
87
87
  doc.SendCommand(lispCode + "\n");
88
- System.Threading.Thread.Sleep(500);
88
+ int retries = 10;
89
+ string result = "";
90
+ while (retries > 0 && !System.IO.File.Exists(tempFile)) {
91
+ System.Threading.Thread.Sleep(200);
92
+ retries--;
93
+ }
89
94
  if (System.IO.File.Exists(tempFile)) {
90
- string result = System.Text.Encoding.GetEncoding("GBK").GetString(System.IO.File.ReadAllBytes(tempFile)).Trim();
91
- System.IO.File.Delete(tempFile);
95
+ retries = 5;
96
+ while (retries > 0) {
97
+ try {
98
+ byte[] bytes = System.IO.File.ReadAllBytes(tempFile);
99
+ result = System.Text.Encoding.GetEncoding("GBK").GetString(bytes).Trim();
100
+ try { System.IO.File.Delete(tempFile); } catch {}
101
+ break;
102
+ } catch (System.IO.IOException) {
103
+ System.Threading.Thread.Sleep(200);
104
+ retries--;
105
+ }
106
+ }
92
107
  if (!string.IsNullOrEmpty(result)) {
93
108
  return new { success = true, result = result };
94
109
  }
@@ -99,17 +114,17 @@ const cadSendWithResult = edge.func(function() {/*
99
114
  }
100
115
  } catch (Exception ex) {
101
116
  System.Diagnostics.Debug.WriteLine("cadSendWithResult error: " + ex.Message);
102
- if (tempFile != null && System.IO.File.Exists(tempFile)) {
103
- try { System.IO.File.Delete(tempFile); } catch {}
117
+ if (tempFile != null) {
118
+ try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
104
119
  }
105
120
  return new { success = false, error = ex.Message };
106
121
  }
107
- if (tempFile != null && System.IO.File.Exists(tempFile)) {
108
- try { System.IO.File.Delete(tempFile); } catch {}
122
+ if (tempFile != null) {
123
+ try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
109
124
  }
110
125
  return new { success = false };
111
126
  }
112
- */});
127
+ */});
113
128
 
114
129
  process.stdin.setEncoding('utf8');
115
130
 
package/src/constants.js CHANGED
@@ -7,7 +7,7 @@ export const FILE_EXTENSIONS = {
7
7
 
8
8
  export const PROTOCOL_VERSION = '2024-11-05';
9
9
  export const SERVER_NAME = 'atlisp-mcp-server';
10
- export const SERVER_VERSION = '1.0.12';
10
+ export const SERVER_VERSION = '1.0.13';
11
11
 
12
12
  export const MOCK_PACKAGES = [
13
13
  { name: 'base', description: '基础函数库', version: '1.0.0' },
@@ -0,0 +1,304 @@
1
+ import { cad } from '../cad.js';
2
+ import { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from '../constants.js';
3
+
4
+ function parseLispList(str) {
5
+ if (!str || typeof str !== 'string') return null;
6
+ const trimmed = str.trim();
7
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return null;
8
+ const content = trimmed.slice(1, -1).trim();
9
+ if (!content) return [];
10
+
11
+ const result = [];
12
+ let depth = 0;
13
+ let current = '';
14
+ let inString = false;
15
+ let escaped = false;
16
+
17
+ for (let i = 0; i < content.length; i++) {
18
+ const ch = content[i];
19
+ if (escaped) {
20
+ current += ch;
21
+ escaped = false;
22
+ continue;
23
+ }
24
+ if (ch === '\\') {
25
+ current += ch;
26
+ escaped = true;
27
+ continue;
28
+ }
29
+ if (ch === '"') {
30
+ current += ch;
31
+ inString = !inString;
32
+ continue;
33
+ }
34
+ if (inString) {
35
+ current += ch;
36
+ continue;
37
+ }
38
+ if (ch === '(' || ch === '[') {
39
+ current += ch;
40
+ depth++;
41
+ } else if (ch === ')' || ch === ']') {
42
+ current += ch;
43
+ depth--;
44
+ } else if (ch === ' ' && depth === 0) {
45
+ if (current.trim()) {
46
+ result.push(current.trim());
47
+ current = '';
48
+ }
49
+ continue;
50
+ }
51
+ current += ch;
52
+ }
53
+ if (current.trim()) result.push(current.trim());
54
+
55
+ return result.map(item => {
56
+ const t = item.trim();
57
+ if (t === 'nil') return null;
58
+ if (t === 't') return true;
59
+ if (t === 'T') return true;
60
+ const num = Number(t);
61
+ if (!isNaN(num) && t !== '') return num;
62
+ if (t.startsWith('"') && t.endsWith('"')) return t.slice(1, -1);
63
+ return t;
64
+ });
65
+ }
66
+
67
+ export const RESOURCES = [
68
+ { uri: 'atlisp://cad/info', name: 'CAD Info', description: 'CAD 连接信息', mimeType: 'application/json', subscribable: true },
69
+ { uri: 'atlisp://cad/layers', name: 'Layers', description: '图层列表', mimeType: 'application/json', subscribable: true },
70
+ { uri: 'atlisp://cad/entities', name: 'Entities', description: '实体统计', mimeType: 'application/json', subscribable: true },
71
+ { uri: 'atlisp://dwg/name', name: 'DWG Name', description: '当前 DWG 文件名', mimeType: 'application/json', subscribable: true },
72
+ { uri: 'atlisp://dwg/path', name: 'DWG Path', description: '文件完整路径', mimeType: 'application/json', subscribable: true },
73
+ { uri: 'atlisp://packages', name: 'Packages', description: '已安装包列表', mimeType: 'application/json', subscribable: true },
74
+ { uri: 'atlisp://platforms', name: 'Platforms', description: '支持的平台信息', mimeType: 'application/json', subscribable: false },
75
+ ];
76
+
77
+ function parseQuery(uri) {
78
+ const qIdx = uri.indexOf('?');
79
+ if (qIdx === -1) return { base: uri, params: {} };
80
+ const base = uri.substring(0, qIdx);
81
+ const params = {};
82
+ uri.substring(qIdx + 1).split('&').forEach(p => {
83
+ const eqIdx = p.indexOf('=');
84
+ const k = eqIdx === -1 ? p : p.substring(0, eqIdx);
85
+ const v = eqIdx === -1 ? '' : p.substring(eqIdx + 1);
86
+ if (k) params[k] = decodeURIComponent(v);
87
+ });
88
+ return { base, params };
89
+ }
90
+
91
+ export async function listResources(subscribedUris = []) {
92
+ return RESOURCES.map(r => ({
93
+ uri: r.uri,
94
+ name: r.name,
95
+ description: r.description,
96
+ mimeType: r.mimeType,
97
+ subscribed: subscribedUris.includes(r.uri)
98
+ }));
99
+ }
100
+
101
+ export async function readResource(uri) {
102
+ const { base, params } = parseQuery(uri);
103
+
104
+ switch (base) {
105
+ case 'atlisp://cad/info':
106
+ return await getCadInfo();
107
+ case 'atlisp://cad/layers':
108
+ return await getLayers(params.name);
109
+ case 'atlisp://cad/entities':
110
+ return await getEntities(params.type);
111
+ case 'atlisp://dwg/name':
112
+ return await getDwgName();
113
+ case 'atlisp://dwg/path':
114
+ return await getDwgPath();
115
+ case 'atlisp://packages':
116
+ return await getPackages(params.name);
117
+ case 'atlisp://platforms':
118
+ return getPlatforms();
119
+ default:
120
+ throw new Error(`资源不存在: ${uri}`);
121
+ }
122
+ }
123
+
124
+ async function getCadInfo() {
125
+ if (!cad.connected) {
126
+ await cad.connect();
127
+ }
128
+
129
+ if (!cad.connected) {
130
+ return {
131
+ connected: false,
132
+ platform: null,
133
+ version: null,
134
+ busy: false
135
+ };
136
+ }
137
+
138
+ let busy = false;
139
+ try {
140
+ busy = await cad.isBusy();
141
+ } catch (e) {}
142
+
143
+ return {
144
+ connected: true,
145
+ platform: cad.getPlatform(),
146
+ version: cad.getVersion(),
147
+ busy
148
+ };
149
+ }
150
+
151
+ async function getLayers(filterName = null) {
152
+ if (!cad.connected) await cad.connect();
153
+ if (!cad.connected) return [];
154
+
155
+ try {
156
+ const code = filterName
157
+ ? `(tblnext "LAYER" T)`
158
+ : `(progn (setq res nil)(while (setq e (tblnext "LAYER" (null res)))(setq res (cons (list (cdr (assoc 2 e)) (cdr (assoc 62 e)) (cdr (assoc 70 e))) res))) (reverse res))`;
159
+
160
+ const result = await cad.sendCommandWithResult(code);
161
+
162
+ if (!result || result === "" || result === "nil") return [];
163
+
164
+ let layers = [];
165
+ try {
166
+ layers = JSON.parse(result);
167
+ } catch (e) {
168
+ const parsed = parseLispList(result);
169
+ if (parsed) {
170
+ layers = parsed;
171
+ } else {
172
+ layers = [];
173
+ }
174
+ }
175
+
176
+ if (!Array.isArray(layers)) {
177
+ if (typeof layers === 'number') {
178
+ layers = [[result, layers, 0]];
179
+ } else if (typeof layers === 'string') {
180
+ layers = [[layers, 7, 0]];
181
+ }
182
+ }
183
+
184
+ return layers.map(l => {
185
+ const name = Array.isArray(l) ? l[0] : l;
186
+ const color = Array.isArray(l) && l[1] !== undefined ? l[1] : 7;
187
+ const flags = Array.isArray(l) && l[2] !== undefined ? l[2] : 0;
188
+ return {
189
+ name: String(name),
190
+ color: color,
191
+ locked: (flags & 4) !== 0,
192
+ frozen: (flags & 1) !== 0
193
+ };
194
+ });
195
+ } catch (e) {
196
+ return [];
197
+ }
198
+ }
199
+
200
+ async function getEntities(filterType = null) {
201
+ if (!cad.connected) await cad.connect();
202
+ if (!cad.connected) return { total: 0, byType: {} };
203
+
204
+ try {
205
+ let code;
206
+ if (filterType) {
207
+ code = `(sslength (ssget "_X" (list (cons 0 "${filterType}"))))`;
208
+ const count = await cad.sendCommandWithResult(code);
209
+ return {
210
+ type: filterType,
211
+ count: parseInt(count) || 0
212
+ };
213
+ } else {
214
+ code = `(progn(setq ss(ssget "_X"))(if ss(progn(setq elst(mapcar (quote(lambda(x)(cdr(assoc 0 (entget x))))) (pickset:to-list ss)))(stat:stat elst))))`;
215
+ const result = await cad.sendCommandWithResult(code);
216
+
217
+ if (!result || result === "" || result === "nil") {
218
+ return { total: 0, byType: {} };
219
+ }
220
+
221
+ let statResult = [];
222
+ try {
223
+ statResult = JSON.parse(result);
224
+ } catch (e) {
225
+ const parsed = parseLispList(result);
226
+ if (parsed) {
227
+ statResult = parsed;
228
+ }
229
+ }
230
+
231
+ if (!Array.isArray(statResult)) {
232
+ return { total: 0, byType: {} };
233
+ }
234
+
235
+ const byType = {};
236
+ statResult.forEach(item => {
237
+ if (Array.isArray(item) && item.length >= 2) {
238
+ byType[item[0]] = item[1];
239
+ }
240
+ });
241
+
242
+ const total = Object.values(byType).reduce((sum, n) => sum + n, 0);
243
+ return { total, byType };
244
+ }
245
+ } catch (e) {
246
+ return { total: 0, byType: {} };
247
+ }
248
+ }
249
+
250
+ async function getDwgName() {
251
+ if (!cad.connected) await cad.connect();
252
+ if (!cad.connected) return { name: null };
253
+
254
+ try {
255
+ const code = `(vl-princ-to-string (getvar "dwgname"))`;
256
+ const name = await cad.sendCommandWithResult(code);
257
+ return { name: name || null };
258
+ } catch (e) {
259
+ return { name: null };
260
+ }
261
+ }
262
+
263
+ async function getDwgPath() {
264
+ if (!cad.connected) await cad.connect();
265
+ if (!cad.connected) return { path: null, name: null };
266
+
267
+ try {
268
+ const code = `(strcat (vl-princ-to-string (getvar "dwgprefix")) (vl-princ-to-string (getvar "dwgname")))`;
269
+ const fullPath = await cad.sendCommandWithResult(code);
270
+
271
+ const nameCode = `(vl-princ-to-string (getvar "dwgname"))`;
272
+ const name = await cad.sendCommandWithResult(nameCode);
273
+
274
+ return {
275
+ path: fullPath || null,
276
+ name: name || null
277
+ };
278
+ } catch (e) {
279
+ return { path: null, name: null };
280
+ }
281
+ }
282
+
283
+ async function getPackages(filterName = null) {
284
+ const packages = MOCK_PACKAGES.map(p => ({
285
+ name: p.name,
286
+ version: p.version,
287
+ description: p.description,
288
+ loaded: true
289
+ }));
290
+
291
+ if (filterName) {
292
+ const pkg = packages.find(p => p.name === filterName);
293
+ return pkg ? [pkg] : [];
294
+ }
295
+
296
+ return packages;
297
+ }
298
+
299
+ function getPlatforms() {
300
+ return CAD_PLATFORMS.map(name => ({
301
+ name,
302
+ extensions: FILE_EXTENSIONS[name] || []
303
+ }));
304
+ }
@@ -0,0 +1,26 @@
1
+ const subscriptions = new Map();
2
+
3
+ export function subscribe(uri) {
4
+ subscriptions.set(uri, Date.now());
5
+ return true;
6
+ }
7
+
8
+ export function unsubscribe(uri) {
9
+ return subscriptions.delete(uri);
10
+ }
11
+
12
+ export function list() {
13
+ return Array.from(subscriptions.keys());
14
+ }
15
+
16
+ export function isSubscribed(uri) {
17
+ return subscriptions.has(uri);
18
+ }
19
+
20
+ export function clear() {
21
+ subscriptions.clear();
22
+ }
23
+
24
+ export function size() {
25
+ return subscriptions.size;
26
+ }