@atlisp/mcp 1.0.15 → 1.0.16

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
@@ -4,10 +4,6 @@ MCP (Model Context Protocol) Server for @lisp on CAD - 为 CAD 环境提供 @lis
4
4
 
5
5
  同时为 AI AGENT 操控 cad 提供调用工具和通道。
6
6
 
7
- ## 版本
8
-
9
- **当前版本**: 1.0.10
10
-
11
7
  ## 功能特性
12
8
 
13
9
  - 连接 AutoCAD/ZWCAD/GStarCAD/BricsCAD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,7 +39,9 @@
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "^1.0.0",
41
41
  "edge-js": "^25.0.1",
42
- "express": "^5.2.1"
42
+ "express": "^5.2.1",
43
+ "iconv-lite": "^0.6.3",
44
+ "raw-body": "^3.0.0"
43
45
  },
44
46
  "engines": {
45
47
  "node": ">=18.0.0"
package/src/atlisp-mcp.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import express from 'express';
2
+ import rawBody from 'raw-body';
3
+ import iconv from 'iconv-lite';
4
+ const { decode: charsetDecode, encodingExists } = iconv;
2
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
7
  import {
6
8
  ListToolsRequestSchema,
@@ -18,7 +20,8 @@ import * as packageHandlers from './handlers/package-handlers.js';
18
20
  import * as functionHandlers from './handlers/function-handlers.js';
19
21
  import { listResources, readResource } from './handlers/resource-handlers.js';
20
22
  import * as subMgr from './subscription-manager.js';
21
- import { createSseResponseHandler } from './sse-utils.js';
23
+ import { SseSession, SSE_EVENTS, createSseResponseHandler, parseLastEventId } from './sse-session.js';
24
+ import { SseSessionManager } from './sse-session-manager.js';
22
25
 
23
26
  const PORT = process.env.PORT || 8110;
24
27
  const HOST = process.env.HOST || '0.0.0.0';
@@ -26,6 +29,8 @@ const TRANSPORT = process.env.TRANSPORT || 'http';
26
29
  const API_KEY = process.env.MCP_API_KEY || '';
27
30
  if (API_KEY) log('API Key authentication enabled');
28
31
 
32
+ const sseSessionManager = new SseSessionManager();
33
+
29
34
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
30
35
 
31
36
  const TOOL_HANDLERS = {
@@ -239,6 +244,83 @@ export function createMcpServer() {
239
244
  return server;
240
245
  }
241
246
 
247
+ async function handleMcpRequest(session, method, params, id) {
248
+ try {
249
+ if (method === 'initialize') {
250
+ session.sendResponse({ jsonrpc: '2.0', id, result: {
251
+ protocolVersion: PROTOCOL_VERSION,
252
+ capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
253
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
254
+ } });
255
+ return;
256
+ }
257
+
258
+ if (method === 'tools/list') {
259
+ session.sendResponse({ jsonrpc: '2.0', id, result: { tools } });
260
+ return;
261
+ }
262
+
263
+ if (method === 'resources/list') {
264
+ const resources = await listResources(subMgr.list());
265
+ session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
266
+ return;
267
+ }
268
+
269
+ if (method === 'resources/read') {
270
+ const uri = params?.uri;
271
+ if (!uri) {
272
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
273
+ return;
274
+ }
275
+ try {
276
+ const data = await readResource(uri);
277
+ session.sendResponse({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } });
278
+ } catch (e) {
279
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
280
+ }
281
+ return;
282
+ }
283
+
284
+ if (method === 'resources/subscribe') {
285
+ const uri = params?.uri;
286
+ if (!uri) {
287
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
288
+ return;
289
+ }
290
+ subMgr.subscribe(uri);
291
+ session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
292
+ return;
293
+ }
294
+
295
+ if (method === 'resources/unsubscribe') {
296
+ const uri = params?.uri;
297
+ if (!uri) {
298
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
299
+ return;
300
+ }
301
+ subMgr.unsubscribe(uri);
302
+ session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
303
+ return;
304
+ }
305
+
306
+ if (method === 'tools/call') {
307
+ const toolName = params?.name;
308
+ if (!toolName) {
309
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
310
+ return;
311
+ }
312
+ const result = await handleToolCall(toolName, params?.arguments || {});
313
+ session.sendResponse({ jsonrpc: '2.0', id, result });
314
+ return;
315
+ }
316
+
317
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
318
+ } catch (e) {
319
+ log('MCP error: ' + e.message);
320
+ session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
321
+ }
322
+ }
323
+
242
324
  export async function startServer() {
243
325
  if (TRANSPORT === 'stdio') {
244
326
  await initCadConnection();
@@ -248,7 +330,24 @@ export async function startServer() {
248
330
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
249
331
  } else {
250
332
  const app = express();
251
- app.use(express.json({ type: ['application/json', 'application/json-rpc'] }));
333
+
334
+ const JSON_CONTENT_TYPES = ['application/json', 'application/json-rpc', 'application/vnd.api+json'];
335
+ app.use(async (req, res, next) => {
336
+ const ct = (req.headers['content-type'] || '').toLowerCase();
337
+ if (!JSON_CONTENT_TYPES.some(t => ct.startsWith(t))) return next();
338
+ try {
339
+ const buf = await rawBody(req, { limit: '10mb' });
340
+ const m = ct.match(/charset\s*=\s*([^\s;]+)/i);
341
+ let charset = m ? m[1].toLowerCase() : 'utf-8';
342
+ const charsetMap = { 'gb2312': 'gbk', 'gb18030': 'gbk', 'gbk': 'gbk', '936': 'gbk' };
343
+ charset = charsetMap[charset] || charset;
344
+ const str = encodingExists(charset) ? charsetDecode(buf, charset) : buf.toString('utf-8');
345
+ req.body = JSON.parse(str);
346
+ next();
347
+ } catch (e) {
348
+ res.status(400).json({ error: 'Invalid JSON body', message: e.message });
349
+ }
350
+ });
252
351
 
253
352
  app.use((req, res, next) => {
254
353
  log(`${req.method} ${req.path} - body: ${JSON.stringify(req.body)}`);
@@ -281,101 +380,148 @@ export async function startServer() {
281
380
 
282
381
  createMcpServer();
283
382
 
284
- const sessionId = crypto.randomUUID();
383
+ app.get('/sse', (req, res) => {
384
+ const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
385
+
386
+ const session = new SseSession(res, { clientId });
387
+
388
+ session.sendEndpoint('/mcp');
389
+ session.sendCapabilities({
390
+ protocolVersion: PROTOCOL_VERSION,
391
+ capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
392
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
393
+ });
394
+
395
+ sseSessionManager.add(clientId, session);
396
+
397
+ res.on('close', () => {
398
+ sseSessionManager.remove(clientId);
399
+ });
400
+ });
285
401
 
286
- ['/sse', '/mcp/sse'].forEach(p => app.all(p, (req, res) => {
287
- res.writeHead(200, { 'Content-Type': 'text/event-stream' });
288
- const transport = new SSEServerTransport('/mcp', res);
289
- server.connect(transport);
402
+ ['/mcp/sse'].forEach(p => app.all(p, (req, res) => {
403
+ const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
404
+
405
+ const session = new SseSession(res, { clientId });
406
+
407
+ session.sendEndpoint('/mcp');
408
+ session.sendCapabilities({
409
+ protocolVersion: PROTOCOL_VERSION,
410
+ capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
411
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
412
+ });
413
+
414
+ sseSessionManager.add(clientId, session);
415
+
416
+ res.on('close', () => {
417
+ sseSessionManager.remove(clientId);
418
+ });
290
419
  }));
291
420
 
292
- app.post('/mcp', async (req, res) => {
293
- const body = req.body || {};
294
- const { method, params, id } = body;
295
- const accept = req.get('Accept') || '';
296
- const wantsSse = accept.includes('text/event-stream');
297
- log('accept: "' + accept + '", wantsSSE: ' + wantsSse);
298
-
299
- res.setHeader('mcp-session-id', sessionId);
300
- const { send } = createSseResponseHandler(res, wantsSse);
301
-
302
- if (!id) return res.status(204).end();
303
-
304
- try {
305
- if (method === 'initialize') {
306
- send({ jsonrpc: '2.0', id, result: {
307
- protocolVersion: PROTOCOL_VERSION,
308
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
309
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
310
- } }, true);
311
- return;
312
- }
313
-
314
- if (method === 'tools/list') {
315
- send({ jsonrpc: '2.0', id, result: { tools } }, true);
316
- return;
317
- }
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
-
362
- if (method === 'tools/call') {
363
- const toolName = params?.name;
364
- if (!toolName) {
365
- send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
366
- return;
367
- }
368
- const result = await handleToolCall(toolName, params?.arguments || {});
369
- send({ jsonrpc: '2.0', id, result }, true);
370
- return;
371
- }
372
-
373
- send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
374
- } catch (e) {
375
- log('MCP error: ' + e.message);
376
- send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
377
- }
378
- });
421
+ app.post('/mcp', async (req, res) => {
422
+ const body = req.body || {};
423
+ const { method, params, id } = body;
424
+ const accept = req.get('Accept') || '';
425
+ const wantsSse = accept.includes('text/event-stream');
426
+ const sessionId = req.get('mcp-session-id') || '';
427
+ log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
428
+
429
+ if (wantsSse) {
430
+ res.setHeader('Content-Type', 'text/event-stream');
431
+ res.setHeader('Cache-Control', 'no-cache');
432
+ res.setHeader('Connection', 'keep-alive');
433
+
434
+ if (sessionId && sseSessionManager.has(sessionId)) {
435
+ const session = sseSessionManager.get(sessionId);
436
+ if (!id) {
437
+ res.status(204).end();
438
+ return;
439
+ }
440
+ await handleMcpRequest(session, method, params, id);
441
+ return;
442
+ }
443
+ }
444
+
445
+ res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
446
+ const { send } = createSseResponseHandler(res, wantsSse);
447
+
448
+ if (!id) return res.status(204).end();
449
+
450
+ try {
451
+ if (method === 'initialize') {
452
+ send({ jsonrpc: '2.0', id, result: {
453
+ protocolVersion: PROTOCOL_VERSION,
454
+ capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
455
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
456
+ } }, true);
457
+ return;
458
+ }
459
+
460
+ if (method === 'tools/list') {
461
+ send({ jsonrpc: '2.0', id, result: { tools } }, true);
462
+ return;
463
+ }
464
+
465
+ if (method === 'resources/list') {
466
+ const resources = await listResources(subMgr.list());
467
+ send({ jsonrpc: '2.0', id, result: { resources } }, true);
468
+ return;
469
+ }
470
+
471
+ if (method === 'resources/read') {
472
+ const uri = params?.uri;
473
+ if (!uri) {
474
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
475
+ return;
476
+ }
477
+ try {
478
+ const data = await readResource(uri);
479
+ send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
480
+ } catch (e) {
481
+ send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
482
+ }
483
+ return;
484
+ }
485
+
486
+ if (method === 'resources/subscribe') {
487
+ const uri = params?.uri;
488
+ if (!uri) {
489
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
490
+ return;
491
+ }
492
+ subMgr.subscribe(uri);
493
+ send({ jsonrpc: '2.0', id, result: { success: true } }, true);
494
+ return;
495
+ }
496
+
497
+ if (method === 'resources/unsubscribe') {
498
+ const uri = params?.uri;
499
+ if (!uri) {
500
+ send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
501
+ return;
502
+ }
503
+ subMgr.unsubscribe(uri);
504
+ send({ jsonrpc: '2.0', id, result: { success: true } }, true);
505
+ return;
506
+ }
507
+
508
+ if (method === 'tools/call') {
509
+ const toolName = params?.name;
510
+ if (!toolName) {
511
+ send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
512
+ return;
513
+ }
514
+ const result = await handleToolCall(toolName, params?.arguments || {});
515
+ send({ jsonrpc: '2.0', id, result }, true);
516
+ return;
517
+ }
518
+
519
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
520
+ } catch (e) {
521
+ log('MCP error: ' + e.message);
522
+ send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
523
+ }
524
+ });
379
525
 
380
526
  app.listen(PORT, HOST, async () => {
381
527
  await initCadConnection();
package/src/cad-worker.js CHANGED
@@ -1,10 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import edge from 'edge-js';
3
3
  import process from 'process';
4
+ import fs from 'fs';
5
+ import path from 'path';
4
6
 
5
7
  const BUSY_RETRIES = parseInt(process.env.BUSY_RETRIES || '10', 10);
6
8
  const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
7
9
 
10
+ const DEBUG_FILE = process.env.DEBUG_FILE || path.join(process.env.TEMP || '/tmp', 'cad-worker-debug.log');
11
+
12
+ function log(msg) {
13
+ const entry = `${new Date().toISOString()} ${msg}\n`;
14
+ fs.appendFileSync(DEBUG_FILE, entry);
15
+ }
16
+
8
17
  const cadConnect = edge.func(function() {/*
9
18
  async (input) => {
10
19
  try {
@@ -111,19 +120,33 @@ const cadSend = edge.func(function() {/*
111
120
  const cadSendWithResult = edge.func(function() {/*
112
121
  async (input) => {
113
122
  string tempFile = null;
123
+ string lispFile = null;
114
124
  try {
115
125
  dynamic d = input;
116
126
  string code = d.code.ToString();
117
127
  string platform = d.platform.ToString();
128
+ string encoding = d.encoding != null ? d.encoding.ToString() : "";
118
129
  string progId = platform + ".Application";
119
- string tempDir = System.IO.Path.GetTempPath();
130
+ string tempDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "@lisp");
131
+ System.IO.Directory.CreateDirectory(tempDir);
132
+
120
133
  tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
121
- string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(print " + code + " f)(close f)(princ))";
134
+ lispFile = System.IO.Path.Combine(tempDir, "atlisp_code_" + System.Guid.NewGuid().ToString("N") + ".lsp");
135
+
136
+ // 生成 LISP 代码:执行 code,将结果打印到 tempFile
137
+ // 使用 princ 输出原始字符串
138
+ string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(princ " + code + " f)(close f)(princ))";
139
+
140
+ // 使用系统 ANSI 编码以兼容 GstarCAD / ZWCAD
141
+ System.IO.File.WriteAllText(lispFile, lispCode, System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage));
142
+
122
143
  dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
123
144
  if (cad != null) {
124
145
  dynamic doc = cad.ActiveDocument;
125
146
  if (doc != null) {
126
- doc.SendCommand(lispCode + "\n");
147
+ // 加载 .lsp 文件执行
148
+ doc.SendCommand("(load \"" + lispFile.Replace("\\", "\\\\") + "\")\n");
149
+
127
150
  int retries = 10;
128
151
  string result = "";
129
152
  while (retries > 0 && !System.IO.File.Exists(tempFile)) {
@@ -135,8 +158,33 @@ const cadSendWithResult = edge.func(function() {/*
135
158
  while (retries > 0) {
136
159
  try {
137
160
  byte[] bytes = System.IO.File.ReadAllBytes(tempFile);
138
- result = System.Text.Encoding.GetEncoding("GBK").GetString(bytes).Trim();
161
+ System.Text.Encoding enc;
162
+
163
+ // 优先使用指定的编码
164
+ if (!string.IsNullOrEmpty(encoding)) {
165
+ enc = System.Text.Encoding.GetEncoding(encoding);
166
+ } else {
167
+ // 自动检测编码:先尝试 UTF-8,如果失败则使用 GBK
168
+ try {
169
+ string utf8Result = System.Text.Encoding.UTF8.GetString(bytes);
170
+ if (!utf8Result.Contains("\ufffd")) {
171
+ enc = System.Text.Encoding.UTF8;
172
+ } else {
173
+ enc = System.Text.Encoding.GetEncoding("gbk");
174
+ }
175
+ } catch {
176
+ try {
177
+ enc = System.Text.Encoding.GetEncoding("gbk");
178
+ } catch {
179
+ enc = System.Text.Encoding.Default;
180
+ }
181
+ }
182
+ }
183
+
184
+ result = enc.GetString(bytes).Trim();
185
+
139
186
  try { System.IO.File.Delete(tempFile); } catch {}
187
+ try { System.IO.File.Delete(lispFile); } catch {}
140
188
  break;
141
189
  } catch (System.IO.IOException) {
142
190
  System.Threading.Thread.Sleep(200);
@@ -156,11 +204,17 @@ const cadSendWithResult = edge.func(function() {/*
156
204
  if (tempFile != null) {
157
205
  try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
158
206
  }
207
+ if (lispFile != null) {
208
+ try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
209
+ }
159
210
  return new { success = false, error = ex.Message };
160
211
  }
161
212
  if (tempFile != null) {
162
213
  try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
163
214
  }
215
+ if (lispFile != null) {
216
+ try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
217
+ }
164
218
  return new { success = false };
165
219
  }
166
220
  */});
@@ -343,10 +397,16 @@ async function handleMessage(msg) {
343
397
  if (!(await waitForIdle(msg.platform))) {
344
398
  return { success: false, error: 'CAD is busy, timeout' };
345
399
  }
400
+ log(`send: ${msg.code}`);
346
401
  return new Promise((resolve, reject) => {
347
402
  cadSend({ code: msg.code, platform: msg.platform }, (e, r) => {
348
- if (e) reject(e);
349
- else resolve(r);
403
+ if (e) {
404
+ log(`send error: ${e.message}`);
405
+ reject(e);
406
+ } else {
407
+ log(`send result: ${JSON.stringify(r)}`);
408
+ resolve(r);
409
+ }
350
410
  });
351
411
  });
352
412
  }
@@ -367,10 +427,16 @@ async function handleMessage(msg) {
367
427
  if (!(await waitForIdle(msg.platform))) {
368
428
  return { success: false, error: 'CAD is busy, timeout' };
369
429
  }
430
+ log(`sendResult: ${msg.code}`);
370
431
  return new Promise((resolve, reject) => {
371
- cadSendWithResult({ code: msg.code, platform: msg.platform }, (e, r) => {
372
- if (e) reject(e);
373
- else resolve(r);
432
+ cadSendWithResult({ code: msg.code, platform: msg.platform, encoding: msg.encoding || '' }, (e, r) => {
433
+ if (e) {
434
+ log(`sendResult error: ${e.message}`);
435
+ reject(e);
436
+ } else {
437
+ log(`sendResult result: ${JSON.stringify(r)}`);
438
+ resolve(r);
439
+ }
374
440
  });
375
441
  });
376
442
  }
package/src/cad.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
7
7
 
8
8
  const MESSAGE_TIMEOUT = parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10);
9
+ const RESULT_ENCODING = process.env.RESULT_ENCODING || '';
9
10
 
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const workerPath = path.join(__dirname, 'cad-worker.js');
@@ -37,9 +38,11 @@ export function getWorker() {
37
38
  worker.connected = true;
38
39
 
39
40
  worker.stderr.on('data', (d) => console.error('worker stderr:', d.toString()));
40
- worker.on('exit', (code) => {
41
+ const w = worker;
42
+ w.on('exit', (code) => {
41
43
  console.error('worker exited:', code);
42
- worker.connected = false;
44
+ if (w !== worker) return;
45
+ w.connected = false;
43
46
  if (heartbeatTimer) {
44
47
  clearInterval(heartbeatTimer);
45
48
  heartbeatTimer = null;
@@ -138,26 +141,18 @@ class CadConnection {
138
141
 
139
142
  async sendCommand(code) {
140
143
  if (!this.connected) throw new Error('未连接 CAD');
141
- try {
142
- const result = await sendMessage({ type: 'send', code: code, platform: _platform });
143
- if (result.success) return true;
144
- throw new Error(result.error || '发送失败');
145
- } catch (e) {
146
- throw e;
147
- }
144
+ const result = await sendMessage({ type: 'send', code: code, platform: _platform });
145
+ if (result.success) return true;
146
+ throw new Error(result.error || '发送失败');
148
147
  }
149
148
 
150
149
  async sendCommandWithResult(code) {
151
150
  if (!this.connected) throw new Error('未连接 CAD');
152
- try {
153
- const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform });
154
- if (result.success) {
155
- return result.result || '';
156
- }
157
- throw new Error(result.error || '发送失败');
158
- } catch (e) {
159
- throw e;
151
+ const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform, encoding: RESULT_ENCODING });
152
+ if (result.success) {
153
+ return result.result || '';
160
154
  }
155
+ throw new Error(result.error || '发送失败');
161
156
  }
162
157
 
163
158
  getVersion() { return this.version; }
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.13';
10
+ export const SERVER_VERSION = '1.0.16';
11
11
 
12
12
  export const MOCK_PACKAGES = [
13
13
  { name: 'base', description: '基础函数库', version: '1.0.0' },
@@ -0,0 +1,135 @@
1
+ import { SseSession, SSE_EVENTS } from './sse-session.js';
2
+
3
+ export class SseSessionManager {
4
+ #sessions = new Map();
5
+ #eventHandlers = new Map();
6
+
7
+ constructor() {
8
+ this.#sessions = new Map();
9
+ this.#eventHandlers = new Map();
10
+ }
11
+
12
+ add(clientId, session) {
13
+ if (!(session instanceof SseSession)) {
14
+ throw new Error('session must be an SseSession instance');
15
+ }
16
+ this.#sessions.set(clientId, session);
17
+ session.startHeartbeat();
18
+ }
19
+
20
+ remove(clientId) {
21
+ const session = this.#sessions.get(clientId);
22
+ if (session) {
23
+ session.close();
24
+ this.#sessions.delete(clientId);
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ get(clientId) {
31
+ return this.#sessions.get(clientId) || null;
32
+ }
33
+
34
+ has(clientId) {
35
+ return this.#sessions.has(clientId);
36
+ }
37
+
38
+ list() {
39
+ return Array.from(this.#sessions.keys()).map(id => ({
40
+ clientId: id,
41
+ isActive: this.#sessions.get(id)?.isActive ?? false
42
+ }));
43
+ }
44
+
45
+ listActive() {
46
+ return this.list().filter(s => s.isActive);
47
+ }
48
+
49
+ count() {
50
+ return this.#sessions.size;
51
+ }
52
+
53
+ sendToClient(clientId, event, data, id = null) {
54
+ const session = this.#sessions.get(clientId);
55
+ if (!session || !session.isActive) {
56
+ return false;
57
+ }
58
+ return session.sendEvent(event, data, id);
59
+ }
60
+
61
+ sendMessageToClient(clientId, data) {
62
+ return this.sendToClient(clientId, SSE_EVENTS.MESSAGE, data);
63
+ }
64
+
65
+ sendErrorToClient(clientId, code, message, id = null) {
66
+ return this.sendToClient(clientId, SSE_EVENTS.ERROR, { code, message }, id);
67
+ }
68
+
69
+ sendProgressToClient(clientId, current, total, message = '') {
70
+ return this.sendToClient(clientId, SSE_EVENTS.PROGRESS, { current, total, message });
71
+ }
72
+
73
+ broadcast(event, data) {
74
+ let successCount = 0;
75
+ for (const [clientId, session] of this.#sessions) {
76
+ if (session.isActive && session.sendEvent(event, data)) {
77
+ successCount++;
78
+ }
79
+ }
80
+ return successCount;
81
+ }
82
+
83
+ broadcastMessage(data) {
84
+ return this.broadcast(SSE_EVENTS.MESSAGE, data);
85
+ }
86
+
87
+ broadcastError(code, message) {
88
+ return this.broadcast(SSE_EVENTS.ERROR, { code, message });
89
+ }
90
+
91
+ on(event, handler) {
92
+ if (!this.#eventHandlers.has(event)) {
93
+ this.#eventHandlers.set(event, new Set());
94
+ }
95
+ this.#eventHandlers.get(event).add(handler);
96
+ }
97
+
98
+ off(event, handler) {
99
+ const handlers = this.#eventHandlers.get(event);
100
+ if (handlers) {
101
+ handlers.delete(handler);
102
+ }
103
+ }
104
+
105
+ emit(event, data) {
106
+ const handlers = this.#eventHandlers.get(event);
107
+ if (handlers) {
108
+ for (const handler of handlers) {
109
+ try {
110
+ handler(data);
111
+ } catch (e) {
112
+ // Ignore handler errors
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ cleanupInactive() {
119
+ for (const [clientId, session] of this.#sessions) {
120
+ if (!session.isActive) {
121
+ session.close();
122
+ this.#sessions.delete(clientId);
123
+ }
124
+ }
125
+ }
126
+
127
+ closeAll() {
128
+ for (const [clientId, session] of this.#sessions) {
129
+ session.close();
130
+ }
131
+ this.#sessions.clear();
132
+ }
133
+ }
134
+
135
+ export const globalSessionManager = new SseSessionManager();
@@ -0,0 +1,204 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ const DEFAULT_HEARTBEAT_INTERVAL = 30000;
4
+ const RECONNECT_DELAY = 5000;
5
+
6
+ export const SSE_EVENTS = {
7
+ MESSAGE: 'message',
8
+ ENDPOINT: 'endpoint',
9
+ PING: 'ping',
10
+ ERROR: 'error',
11
+ PROGRESS: 'progress',
12
+ CAPABILITIES: 'capabilities',
13
+ };
14
+
15
+ export class SseSession {
16
+ #res;
17
+ #clientId;
18
+ #active = true;
19
+ #heartbeatTimer = null;
20
+ #lastEventId = 0;
21
+ #heartbeatInterval;
22
+ #sessionData = {};
23
+
24
+ constructor(res, options = {}) {
25
+ this.#res = res;
26
+ this.#clientId = options.clientId || randomUUID();
27
+ this.#heartbeatInterval = options.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
28
+ this.#sessionData = options.sessionData || {};
29
+
30
+ this._setupSseHeaders();
31
+ }
32
+
33
+ get clientId() {
34
+ return this.#clientId;
35
+ }
36
+
37
+ get isActive() {
38
+ return this.#active;
39
+ }
40
+
41
+ get sessionData() {
42
+ return this.#sessionData;
43
+ }
44
+
45
+ setSessionData(key, value) {
46
+ this.#sessionData[key] = value;
47
+ }
48
+
49
+ getSessionData(key) {
50
+ return this.#sessionData[key];
51
+ }
52
+
53
+ _setupSseHeaders() {
54
+ this.#res.setHeader('Content-Type', 'text/event-stream');
55
+ this.#res.setHeader('Cache-Control', 'no-cache');
56
+ this.#res.setHeader('Connection', 'keep-alive');
57
+ this.#res.setHeader('Transfer-Encoding', 'chunked');
58
+ this.#res.setHeader('X-Accel-Buffering', 'no');
59
+ }
60
+
61
+ _formatEvent(event, data, id = null) {
62
+ let lines = [];
63
+ if (id !== null) {
64
+ lines.push(`id: ${id}`);
65
+ }
66
+ lines.push(`event: ${event}`);
67
+
68
+ const jsonStr = JSON.stringify(data);
69
+ lines.push(`data: ${jsonStr}`);
70
+
71
+ return lines.join('\n') + '\n\n';
72
+ }
73
+
74
+ _write(content) {
75
+ if (!this.#active) return false;
76
+ try {
77
+ const result = this.#res.write(content);
78
+ if (typeof this.#res.flush === 'function') {
79
+ this.#res.flush();
80
+ }
81
+ if (result === false) {
82
+ this.#active = false;
83
+ return false;
84
+ }
85
+ return true;
86
+ } catch (e) {
87
+ this.#active = false;
88
+ return false;
89
+ }
90
+ }
91
+
92
+ sendEvent(event, data, id = null) {
93
+ this.#lastEventId++;
94
+ const eventId = id !== null ? id : this.#lastEventId;
95
+ return this._write(this._formatEvent(event, data, eventId));
96
+ }
97
+
98
+ sendMessage(data) {
99
+ return this.sendEvent(SSE_EVENTS.MESSAGE, data);
100
+ }
101
+
102
+ sendEndpoint(path) {
103
+ return this.sendEvent(SSE_EVENTS.ENDPOINT, { path });
104
+ }
105
+
106
+ sendError(code, message, id = null) {
107
+ return this.sendEvent(SSE_EVENTS.ERROR, { code, message }, id);
108
+ }
109
+
110
+ sendPing() {
111
+ return this.sendEvent(SSE_EVENTS.PING, { timestamp: Date.now() });
112
+ }
113
+
114
+ sendProgress(current, total, message = '') {
115
+ return this.sendEvent(SSE_EVENTS.PROGRESS, { current, total, message });
116
+ }
117
+
118
+ sendCapabilities(capabilities) {
119
+ return this.sendEvent(SSE_EVENTS.CAPABILITIES, capabilities);
120
+ }
121
+
122
+ sendResponse(jsonrpcResponse, id = null) {
123
+ const eventId = id !== null ? id : this.#lastEventId;
124
+ return this.sendEvent(SSE_EVENTS.MESSAGE, jsonrpcResponse, eventId);
125
+ }
126
+
127
+ startHeartbeat(interval = null) {
128
+ this.stopHeartbeat();
129
+ const ms = interval || this.#heartbeatInterval;
130
+
131
+ this.#heartbeatTimer = setInterval(() => {
132
+ if (!this.#active) {
133
+ this.stopHeartbeat();
134
+ return;
135
+ }
136
+ if (!this.sendPing()) {
137
+ this.stopHeartbeat();
138
+ this.close();
139
+ }
140
+ }, ms);
141
+ }
142
+
143
+ stopHeartbeat() {
144
+ if (this.#heartbeatTimer) {
145
+ clearInterval(this.#heartbeatTimer);
146
+ this.#heartbeatTimer = null;
147
+ }
148
+ }
149
+
150
+ close() {
151
+ this.#active = false;
152
+ this.stopHeartbeat();
153
+ try {
154
+ this.#res.end();
155
+ } catch (e) {
156
+ // Ignore errors during close
157
+ }
158
+ }
159
+ }
160
+
161
+ export function setupSseHeaders(res) {
162
+ res.setHeader('Content-Type', 'text/event-stream');
163
+ res.setHeader('Cache-Control', 'no-cache');
164
+ res.setHeader('Connection', 'keep-alive');
165
+ res.setHeader('Transfer-Encoding', 'chunked');
166
+ }
167
+
168
+ export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE) {
169
+ const lines = [
170
+ `event: ${event}`,
171
+ `data: ${JSON.stringify(data)}`,
172
+ ''
173
+ ];
174
+ res.write(lines.join('\n') + '\n');
175
+ if (typeof res.flush === 'function') res.flush();
176
+ }
177
+
178
+ export function createSseResponseHandler(res, acceptSse = false) {
179
+ if (acceptSse) {
180
+ setupSseHeaders(res);
181
+ }
182
+
183
+ return {
184
+ send: (data, done = false) => {
185
+ const json = JSON.stringify(data);
186
+ if (acceptSse) {
187
+ sendSSE(res, data);
188
+ if (done) res.end();
189
+ } else {
190
+ res.setHeader('Content-Type', 'application/json');
191
+ res.end(json);
192
+ }
193
+ }
194
+ };
195
+ }
196
+
197
+ export function parseLastEventId(req) {
198
+ const lastEventId = req.get('Last-Event-ID') || req.get('last-event-id');
199
+ if (lastEventId) {
200
+ const parsed = parseInt(lastEventId, 10);
201
+ return isNaN(parsed) ? null : parsed;
202
+ }
203
+ return null;
204
+ }
package/src/sse-utils.js DELETED
@@ -1,32 +0,0 @@
1
- export function setupSseHeaders(res) {
2
- res.setHeader('Content-Type', 'text/event-stream');
3
- res.setHeader('Cache-Control', 'no-cache');
4
- res.setHeader('Connection', 'keep-alive');
5
- res.setHeader('Transfer-Encoding', 'chunked');
6
- }
7
-
8
- export function sendSSE(res, data, event = 'message') {
9
- const json = JSON.stringify(data);
10
- res.write(`event: ${event}\n`);
11
- res.write(`data: ${json}\n\n`);
12
- if (typeof res.flush === 'function') res.flush();
13
- }
14
-
15
- export function createSseResponseHandler(res, acceptSse = false) {
16
- if (acceptSse) {
17
- setupSseHeaders(res);
18
- }
19
-
20
- return {
21
- send: (data, done = false) => {
22
- const json = JSON.stringify(data);
23
- if (acceptSse) {
24
- sendSSE(res, data);
25
- if (done) res.end();
26
- } else {
27
- res.setHeader('Content-Type', 'application/json');
28
- res.end(json);
29
- }
30
- }
31
- };
32
- }