@atlisp/mcp 1.1.3 → 1.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
@@ -254,6 +254,7 @@ curl http://localhost:8110/health
254
254
  | `install_atlisp` | 安装 @lisp | 无 |
255
255
  | `get_function_usage` | 获取函数用法 | `name`: 函数名 |
256
256
  | `list_functions` | 列出所有函数 | 无 |
257
+ | `load_atlisp_functionlib` | 从网络加载函数库(AI 提示词/编程) | `format`: json/list |
257
258
 
258
259
  ### MCP 方法
259
260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
package/src/atlisp-mcp.js CHANGED
@@ -55,11 +55,11 @@ import rawBody from 'raw-body';
55
55
  import iconv from 'iconv-lite';
56
56
  import rateLimit from 'express-rate-limit';
57
57
  import { randomUUID } from 'crypto';
58
+ import { AsyncLocalStorage } from 'async_hooks';
58
59
  const { decode: charsetDecode, encodingExists } = iconv;
59
60
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
60
61
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
61
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
62
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
62
+ import { SessionTransport } from './session-transport.js';
63
63
  import {
64
64
  ListToolsRequestSchema,
65
65
  CallToolRequestSchema,
@@ -79,9 +79,14 @@ import * as functionHandlers from './handlers/function-handlers.js';
79
79
  import { listResources, readResource } from './handlers/resource-handlers.js';
80
80
  import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
81
81
  import * as subMgr from './subscription-manager.js';
82
- import { createError, toMcpError } from './errors.js';
82
+ import { sessionManager } from './subscription-manager.js';
83
+ import { SseSessionManager } from './sse-session-manager.js';
84
+ import { SseSession } from './sse-session.js';
83
85
  import config from './config.js';
84
86
 
87
+ const mcpContext = new AsyncLocalStorage();
88
+ const sseSessionManager = new SseSessionManager();
89
+
85
90
  const SERVER_CAPABILITIES = {
86
91
  tools: {},
87
92
  resources: { subscribe: true, listChanged: true },
@@ -108,6 +113,30 @@ const mcpLimiter = rateLimit({
108
113
 
109
114
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
110
115
 
116
+ const FUNCTION_LIB_URL = process.env.FUNCTION_LIB_URL || 'http://s3.atlisp.cn/json/functions.json';
117
+ let cachedFunctionLib = null;
118
+ let cachedAt = null;
119
+ const CACHE_TTL = 5 * 60 * 1000;
120
+
121
+ export async function loadAtlibFunctionLib() {
122
+ const now = Date.now();
123
+ if (cachedFunctionLib && cachedAt && (now - cachedAt) < CACHE_TTL) {
124
+ return cachedFunctionLib;
125
+ }
126
+ try {
127
+ const response = await fetch(FUNCTION_LIB_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
128
+ if (response.ok) {
129
+ const data = await response.json();
130
+ cachedFunctionLib = data;
131
+ cachedAt = now;
132
+ return data;
133
+ }
134
+ } catch (e) {
135
+ log(`loadAtlibFunctionLib error: ${e.message}`);
136
+ }
137
+ return null;
138
+ }
139
+
111
140
  const TOOL_HANDLERS = {
112
141
  connect_cad: () => cadHandlers.connectCad(),
113
142
  eval_lisp: (a) => cadHandlers.evalLisp(a.code),
@@ -124,19 +153,48 @@ const TOOL_HANDLERS = {
124
153
  init_atlisp: () => cadHandlers.initAtlisp(),
125
154
  get_function_usage: (a) => functionHandlers.getFunctionUsage(a.name, a.package),
126
155
  list_functions: (a) => functionHandlers.listFunctions(a.package),
127
- get_file_extensions: (a) => {
128
- const exts = FILE_EXTENSIONS[a.platform];
129
- if (!exts) return { content: [{ type: 'text', text: `平台 ${a.platform} 不支持` }], isError: true };
130
- return { content: [{ type: 'text', text: `${a.platform} 文件扩展名: ${exts.join(', ')}` }] };
156
+ load_atlisp_functionlib: async (a) => {
157
+ const data = await loadAtlibFunctionLib();
158
+ if (!data) return { content: [{ type: 'text', text: '加载函数库失败' }], isError: true };
159
+ if (a.format === 'list') {
160
+ const items = data.functions?.map(f => `${f.name}: ${f.description || ''}`) || [];
161
+ return { content: [{ type: 'text', text: items.join('\n') }] };
162
+ }
163
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
131
164
  },
132
- get_install_code: () => ({
133
- content: [{
134
- type: 'text',
135
- text: `复制以下代码到 CAD 命令行加载:\n(load "atlisp" "atlisp")\n(vlax-create-object "atlisp.atlisp.1")`
136
- }]
137
- }),
138
165
  };
139
166
 
167
+ function getOrCreateSessionServer(sessionId) {
168
+ let entry = sessionServers.get(sessionId);
169
+ if (!entry) {
170
+ const server = createMcpServer();
171
+ const transport = new SessionTransport(sessionId);
172
+ server.connect(transport);
173
+ entry = { server, transport };
174
+ sessionServers.set(sessionId, entry);
175
+ sessionTranports.set(sessionId, transport);
176
+ }
177
+ return entry;
178
+ }
179
+
180
+ function removeSessionServer(sessionId) {
181
+ const entry = sessionServers.get(sessionId);
182
+ if (entry) {
183
+ entry.transport.close().catch(() => {});
184
+ entry.server.close().catch(() => {});
185
+ sessionServers.delete(sessionId);
186
+ sessionTranports.delete(sessionId);
187
+ }
188
+ }
189
+
190
+ function broadcastToAllSessions(uri) {
191
+ for (const [sid, entry] of sessionServers) {
192
+ entry.server.sendResourceUpdated({ uri }).catch(err => {
193
+ log(`Failed to send resource update to session ${sid}: ${err.message}`);
194
+ });
195
+ }
196
+ }
197
+
140
198
  export const tools = [
141
199
  {
142
200
  name: 'connect_cad',
@@ -228,7 +286,7 @@ export const tools = [
228
286
  },
229
287
  {
230
288
  name: 'get_function_usage',
231
- description: '获取 @lisp 函数用法(从本地 JSON 库)',
289
+ description: '获取 @lisp 函数用法',
232
290
  inputSchema: {
233
291
  type: 'object',
234
292
  properties: {
@@ -249,20 +307,14 @@ export const tools = [
249
307
  }
250
308
  },
251
309
  {
252
- name: 'get_file_extensions',
253
- description: '获取 CAD 平台对应的文件扩展名',
310
+ name: 'load_atlisp_functionlib',
311
+ description: ' http://s3.atlisp.cn/json/functions.json 加载 @lisp 函数库(用于 AI Agent 的提示词和编程调用)',
254
312
  inputSchema: {
255
313
  type: 'object',
256
314
  properties: {
257
- platform: { type: 'string', description: 'CAD 平台,如 AutoCAD, ZWCAD, GStarCAD, BricsCAD' }
258
- },
259
- required: ['platform']
315
+ format: { type: 'string', enum: ['json', 'list'], description: '返回格式:json(完整JSON)或 list(函数名:描述 列表),默认 json' }
316
+ }
260
317
  }
261
- },
262
- {
263
- name: 'get_install_code',
264
- description: '获取 @lisp 安装代码',
265
- inputSchema: { type: 'object', properties: {} }
266
318
  }
267
319
  ];
268
320
 
@@ -311,10 +363,11 @@ async function initCadConnection() {
311
363
  }
312
364
  }
313
365
 
314
- let server = null;
366
+ const sessionServers = new Map();
367
+ const sessionTranports = new Map();
315
368
 
316
369
  export function createMcpServer() {
317
- server = new Server(
370
+ const server = new Server(
318
371
  { name: SERVER_NAME, version: SERVER_VERSION },
319
372
  { capabilities: SERVER_CAPABILITIES }
320
373
  );
@@ -326,9 +379,14 @@ export function createMcpServer() {
326
379
  return await handleToolCall(name, args || {});
327
380
  });
328
381
 
329
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
330
- resources: await listResources(subMgr.list())
331
- }));
382
+ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
383
+ const ctx = mcpContext.getStore();
384
+ const sessionId = ctx?.sessionId;
385
+ const subscribedUris = sessionId ? subMgr.list(sessionId) : subMgr.list();
386
+ return {
387
+ resources: await listResources(subscribedUris)
388
+ };
389
+ });
332
390
 
333
391
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
334
392
  const { uri } = request.params;
@@ -352,13 +410,15 @@ export function createMcpServer() {
352
410
 
353
411
  server.setRequestHandler(SubscribeRequestSchema, async (request) => {
354
412
  const { uri } = request.params;
355
- subMgr.subscribe(uri);
413
+ const ctx = mcpContext.getStore();
414
+ subMgr.subscribe(uri, ctx?.sessionId);
356
415
  return { success: true };
357
416
  });
358
417
 
359
418
  server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
360
419
  const { uri } = request.params;
361
- subMgr.unsubscribe(uri);
420
+ const ctx = mcpContext.getStore();
421
+ subMgr.unsubscribe(uri, ctx?.sessionId);
362
422
  return { success: true };
363
423
  });
364
424
 
@@ -374,74 +434,18 @@ export function createMcpServer() {
374
434
  return server;
375
435
  }
376
436
 
377
- async function dispatchMcpMethod(responder, method, params, id) {
378
- try {
379
- if (method === 'initialize') {
380
- responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
381
- return;
382
- }
383
-
384
- const methodHandlers = {
385
- 'tools/list': async () => ({ tools }),
386
- 'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
387
- 'resources/read': async () => {
388
- const uri = params?.uri;
389
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
390
- const data = await readResource(uri);
391
- return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
392
- },
393
- 'resources/subscribe': async () => {
394
- const uri = params?.uri;
395
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
396
- subMgr.subscribe(uri);
397
- return { success: true };
398
- },
399
- 'resources/unsubscribe': async () => {
400
- const uri = params?.uri;
401
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
402
- subMgr.unsubscribe(uri);
403
- return { success: true };
404
- },
405
- 'tools/call': async () => {
406
- const toolName = params?.name;
407
- if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
408
- return await handleToolCall(toolName, params?.arguments || {});
409
- },
410
- 'prompts/list': async () => ({ prompts: await listPrompts() }),
411
- 'prompts/get': async () => {
412
- const promptName = params?.name;
413
- if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
414
- return await getPrompt(promptName, params?.arguments || {});
415
- }
416
- };
417
-
418
- const handler = methodHandlers[method];
419
- if (handler) {
420
- const result = await handler();
421
- responder({ jsonrpc: '2.0', id, result });
422
- } else {
423
- responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
424
- }
425
- } catch (e) {
426
- const mcpError = toMcpError(e);
427
- log(`MCP error [${method}]: ${mcpError.message}`);
428
- responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
429
- }
430
- }
431
437
 
432
- async function handleMcpRequest(session, method, params, id) {
433
- await dispatchMcpMethod(
434
- (response) => session.sendResponse(response),
435
- method, params, id
436
- );
437
- }
438
438
 
439
439
  export async function startServer() {
440
440
  if (config.transport === 'stdio') {
441
441
  await initCadConnection();
442
442
  const mcpServer = createMcpServer();
443
443
  const transport = new StdioServerTransport();
444
- await mcpServer.connect(transport);
444
+ const sessionId = 'stdio-session';
445
+ sessionManager.createSession(sessionId);
446
+ await mcpContext.run({ sessionId }, async () => {
447
+ await mcpServer.connect(transport);
448
+ });
445
449
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
446
450
 
447
451
  subMgr.setNotify((uri, data) => {
@@ -502,129 +506,115 @@ export async function startServer() {
502
506
  });
503
507
  }
504
508
 
505
- const mcpServer = createMcpServer();
506
-
507
- const transport = new StreamableHTTPServerTransport({
508
- sessionIdGenerator: () => randomUUID(),
509
- });
510
-
511
- await mcpServer.connect(transport);
512
- log('MCP Server connected to StreamableHTTPServerTransport');
513
-
514
509
  subMgr.setNotify((uri, data) => {
515
- mcpServer.sendResourceUpdated({ uri }).catch(err => {
516
- log(`Failed to send resource update: ${err.message}`);
517
- });
510
+ broadcastToAllSessions(uri);
511
+ for (const session of sseSessionManager.listActive()) {
512
+ sseSessionManager.sendToClient(session.clientId, 'resource_updated', { uri });
513
+ }
518
514
  });
519
515
 
520
- app.all('/mcp', mcpLimiter, async (req, res) => {
521
- try {
522
- await transport.handleRequest(req, res, req.body);
523
- } catch (error) {
524
- log(`Transport error: ${error.message}`);
525
- if (!res.headersSent) {
526
- res.status(500).json({ error: 'Internal server error' });
527
- }
516
+ async function handleMcpPost(req, res) {
517
+ const sessionId = req.headers['mcp-session-id'] || req.query.sessionId || randomUUID();
518
+ sessionManager.createSession(sessionId);
519
+ const body = req.body;
520
+ if (!body || !body.method) {
521
+ return res.status(400).json({ error: 'Invalid request' });
528
522
  }
529
- });
530
523
 
531
- app.all('/mcp/:path', mcpLimiter, async (req, res) => {
532
- try {
533
- await transport.handleRequest(req, res, req.body);
534
- } catch (error) {
535
- log(`Transport error: ${error.message}`);
536
- if (!res.headersSent) {
537
- res.status(500).json({ error: 'Internal server error' });
524
+ const { server, transport } = getOrCreateSessionServer(sessionId);
525
+ transport.resetCleanupTimer();
526
+
527
+ await mcpContext.run({ sessionId }, async () => {
528
+ try {
529
+ const response = await transport.handleRequest(body);
530
+ if (response) {
531
+ const headers = { 'Content-Type': 'application/json' };
532
+ if (transport.sessionId) {
533
+ headers['mcp-session-id'] = transport.sessionId;
534
+ }
535
+ res.writeHead(200, headers);
536
+ res.end(JSON.stringify(response));
537
+ } else {
538
+ if (!res.headersSent) {
539
+ res.status(202).end();
540
+ }
541
+ }
542
+ } catch (error) {
543
+ log(`Transport error [${sessionId}]: ${error.message}`);
544
+ if (!res.headersSent) {
545
+ res.status(500).json({ error: 'Internal server error', message: error.message });
546
+ }
538
547
  }
539
- }
540
- });
548
+ });
549
+ }
550
+
551
+ app.all('/mcp', mcpLimiter, handleMcpPost);
552
+ app.all('/mcp/:path', mcpLimiter, handleMcpPost);
553
+ app.get('/mcp/stream', mcpLimiter, handleMcpPost);
541
554
 
542
555
  const sseEndpoint = '/sse';
543
556
  app.get(sseEndpoint, async (req, res) => {
544
- const sseTransport = new SSEServerTransport(sseEndpoint, res);
545
- try {
546
- await mcpServer.connect(sseTransport);
547
- await sseTransport.handleRequest(req, res);
548
- } catch (error) {
549
- log(`SSE transport error: ${error.message}`);
550
- if (!res.headersSent) {
551
- res.status(500).json({ error: 'Internal server error' });
552
- }
553
- }
554
- });
555
- app.post(sseEndpoint, mcpLimiter, async (req, res) => {
556
- const sessionId = req.headers['mcp-session-id'];
557
- const sseTransport = sessionId ? sseEndpoint + '?sessionId=' + sessionId : sseEndpoint;
558
- try {
559
- const transport = new SSEServerTransport(sseEndpoint, res);
560
- await mcpServer.connect(transport);
561
- await transport.handleRequest(req, res);
562
- } catch (error) {
563
- log(`SSE POST error: ${error.message}`);
564
- if (!res.headersSent) {
565
- res.status(500).json({ error: 'Internal server error' });
566
- }
567
- }
568
- });
557
+ const sessionId = randomUUID();
558
+ sessionManager.createSession(sessionId);
559
+ getOrCreateSessionServer(sessionId);
569
560
 
570
- app.get('/mcp/sse', async (req, res) => {
571
- const sseTransport = new SSEServerTransport('/mcp/sse', res);
572
- try {
573
- await mcpServer.connect(sseTransport);
574
- await sseTransport.handleRequest(req, res);
575
- } catch (error) {
576
- log(`/mcp/sse transport error: ${error.message}`);
577
- if (!res.headersSent) {
578
- res.status(500).json({ error: 'Internal server error' });
579
- }
580
- }
581
- });
582
- app.post('/mcp/sse', mcpLimiter, async (req, res) => {
583
- const sseTransport = new SSEServerTransport('/mcp/sse', res);
584
- try {
585
- await mcpServer.connect(sseTransport);
586
- await sseTransport.handleRequest(req, res);
587
- } catch (error) {
588
- log(`/mcp/sse POST error: ${error.message}`);
589
- if (!res.headersSent) {
590
- res.status(500).json({ error: 'Internal server error' });
591
- }
592
- }
593
- });
561
+ const sseSession = new SseSession(res, { clientId: sessionId });
562
+ sseSessionManager.add(sessionId, sseSession);
594
563
 
595
- app.get('/mcp/stream', async (req, res) => {
596
- try {
597
- await transport.handleRequest(req, res, req.body);
598
- } catch (error) {
599
- log(`/mcp/stream error: ${error.message}`);
600
- if (!res.headersSent) {
601
- res.status(500).json({ error: 'Internal server error' });
602
- }
603
- }
564
+ sseSession.sendEndpoint(`/message?sessionId=${sessionId}`);
565
+
566
+ req.on('close', () => {
567
+ sseSessionManager.remove(sessionId);
568
+ removeSessionServer(sessionId);
569
+ });
604
570
  });
605
571
 
606
572
  app.post('/message', mcpLimiter, async (req, res) => {
607
- try {
608
- const sessionId = req.headers['mcp-session-id'];
609
- const sseTransport = new SSEServerTransport('/message', res);
610
- await mcpServer.connect(sseTransport);
611
- await sseTransport.handleRequest(req, res);
612
- } catch (error) {
613
- log(`/message error: ${error.message}`);
614
- if (!res.headersSent) {
615
- res.status(500).json({ error: 'Internal server error' });
573
+ const sessionId = req.query.sessionId;
574
+ if (!sessionId) {
575
+ return res.status(400).json({ error: 'Missing sessionId' });
576
+ }
577
+ const sseSession = sseSessionManager.get(sessionId);
578
+ if (!sseSession) {
579
+ return res.status(404).json({ error: 'Session not found' });
580
+ }
581
+ sessionManager.createSession(sessionId);
582
+ const { server, transport } = getOrCreateSessionServer(sessionId);
583
+ transport.resetCleanupTimer();
584
+
585
+ const body = req.body;
586
+ if (body && body.method) {
587
+ try {
588
+ const response = await transport.handleRequest(body);
589
+ if (response) {
590
+ sseSession.sendResponse(response);
591
+ }
592
+ if (!res.headersSent) {
593
+ res.status(202).end();
594
+ }
595
+ } catch (error) {
596
+ log(`SSE message error [${sessionId}]: ${error.message}`);
597
+ if (!res.headersSent) {
598
+ res.status(500).json({ error: 'Internal server error' });
599
+ }
616
600
  }
601
+ } else {
602
+ res.status(400).json({ error: 'Invalid request' });
617
603
  }
618
604
  });
619
605
 
620
606
  const httpServer = app.listen(config.port, config.host, async () => {
621
607
  await initCadConnection();
622
- console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP (SDK)`);
608
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} (多会话模式)`);
623
609
  });
624
610
 
625
611
  async function shutdown(signal) {
626
612
  console.error(`\n收到 ${signal},正在优雅关闭...`);
627
- await transport.close();
613
+ sseSessionManager.closeAll();
614
+ sessionManager.clear();
615
+ for (const [sid] of sessionServers) {
616
+ removeSessionServer(sid);
617
+ }
628
618
  await cad.disconnect();
629
619
  closeLog();
630
620
  httpServer.close(() => process.exit(0));
package/src/cad-worker.js CHANGED
@@ -244,10 +244,11 @@ process.stdin.on('data', (data) => {
244
244
  if (!line.trim()) continue;
245
245
  try {
246
246
  const msg = JSON.parse(line);
247
+ const requestId = msg.requestId;
247
248
  handleMessage(msg).then(result => {
248
- process.stdout.write(JSON.stringify(result) + '\n');
249
+ process.stdout.write(JSON.stringify({ ...result, requestId }) + '\n');
249
250
  }).catch(err => {
250
- process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
251
+ process.stdout.write(JSON.stringify({ error: err.message, requestId }) + '\n');
251
252
  });
252
253
  } catch (e) {
253
254
  process.stdout.write(JSON.stringify({ error: 'invalid message' }) + '\n');
package/src/cad.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { randomUUID } from 'crypto';
4
5
  import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
5
6
  import config from './config.js';
6
7
 
@@ -15,8 +16,19 @@ let _platform = null;
15
16
  let _version = null;
16
17
 
17
18
  let heartbeatTimer = null;
19
+ const pendingRequests = new Map();
20
+ let stdoutHandlerSetup = false;
21
+
22
+ const MAX_BUFFER_SIZE = 1024 * 1024;
18
23
 
19
24
  export function resetWorker() {
25
+ for (const [id, { reject, timeout }] of pendingRequests) {
26
+ clearTimeout(timeout);
27
+ reject(new Error('Worker reset'));
28
+ }
29
+ pendingRequests.clear();
30
+ stdoutHandlerSetup = false;
31
+
20
32
  if (worker) {
21
33
  worker.kill();
22
34
  worker = null;
@@ -27,6 +39,43 @@ export function resetWorker() {
27
39
  }
28
40
  }
29
41
 
42
+ function setupStdoutHandler(w) {
43
+ let buffer = '';
44
+ stdoutHandlerSetup = true;
45
+
46
+ w.stdout.on('data', (data) => {
47
+ try {
48
+ buffer += data.toString();
49
+ if (buffer.length > MAX_BUFFER_SIZE) {
50
+ buffer = buffer.slice(-MAX_BUFFER_SIZE);
51
+ }
52
+ const lines = buffer.split('\n');
53
+ buffer = lines.pop() || '';
54
+ for (const line of lines) {
55
+ if (!line.trim()) continue;
56
+ try {
57
+ const result = JSON.parse(line);
58
+ const rid = result.requestId;
59
+ if (rid && pendingRequests.has(rid)) {
60
+ const { resolve, reject, timeout } = pendingRequests.get(rid);
61
+ clearTimeout(timeout);
62
+ pendingRequests.delete(rid);
63
+ if (result.error) {
64
+ reject(new Error(result.error));
65
+ } else {
66
+ resolve(result);
67
+ }
68
+ }
69
+ } catch (parseErr) {
70
+ console.error('JSON parse error in worker output:', parseErr.message);
71
+ }
72
+ }
73
+ } catch (e) {
74
+ console.error('Unexpected error in worker stdout handler:', e.message);
75
+ }
76
+ });
77
+ }
78
+
30
79
  export function getWorker() {
31
80
  if (!worker || !worker.connected) {
32
81
  if (worker) worker.kill();
@@ -34,18 +83,27 @@ export function getWorker() {
34
83
  stdio: ['pipe', 'pipe', 'pipe']
35
84
  });
36
85
  worker.connected = true;
37
-
86
+
38
87
  worker.stderr.on('data', (d) => console.error('worker stderr:', d.toString()));
39
88
  const w = worker;
40
89
  w.on('exit', (code) => {
41
90
  console.error('worker exited:', code);
42
91
  if (w !== worker) return;
43
92
  w.connected = false;
93
+
94
+ for (const [id, { reject, timeout }] of pendingRequests) {
95
+ clearTimeout(timeout);
96
+ reject(new Error(`Worker exited with code ${code}`));
97
+ }
98
+ pendingRequests.clear();
99
+ stdoutHandlerSetup = false;
100
+
44
101
  if (heartbeatTimer) {
45
102
  clearInterval(heartbeatTimer);
46
103
  heartbeatTimer = null;
47
104
  }
48
105
  });
106
+ setupStdoutHandler(w);
49
107
  startHeartbeat();
50
108
  }
51
109
  return worker;
@@ -68,53 +126,26 @@ function startHeartbeat() {
68
126
  }
69
127
 
70
128
  export function sendMessage(msg) {
71
- return new Promise((resolve, reject) => {
72
- const w = getWorker();
73
- let responded = false;
74
- let buffer = '';
129
+ const w = getWorker();
130
+ const requestId = randomUUID();
75
131
 
132
+ return new Promise((resolve, reject) => {
76
133
  const timeout = setTimeout(() => {
77
- if (!responded) {
78
- responded = true;
134
+ const pending = pendingRequests.get(requestId);
135
+ if (pending) {
136
+ pendingRequests.delete(requestId);
79
137
  reject(new Error('Timeout waiting for worker'));
80
138
  }
81
139
  }, MESSAGE_TIMEOUT);
82
140
 
83
- const handler = (data) => {
84
- try {
85
- buffer += data.toString();
86
- const lines = buffer.split('\n');
87
- buffer = lines.pop() || '';
88
- for (const line of lines) {
89
- if (!line.trim()) continue;
90
- try {
91
- const result = JSON.parse(line);
92
- if (result.error) {
93
- if (!responded) {
94
- responded = true;
95
- clearTimeout(timeout);
96
- w.stdout.off('data', handler);
97
- reject(new Error(result.error));
98
- }
99
- } else {
100
- if (!responded) {
101
- responded = true;
102
- clearTimeout(timeout);
103
- w.stdout.off('data', handler);
104
- resolve(result);
105
- }
106
- }
107
- } catch (parseErr) {
108
- console.error('JSON parse error in worker output:', parseErr.message, 'line:', line.slice(0, 200));
109
- }
110
- }
111
- } catch (e) {
112
- console.error('Unexpected error in worker stdout handler:', e.message);
113
- }
114
- };
115
-
116
- w.stdout.on('data', handler);
117
- w.stdin.write(JSON.stringify(msg) + '\n');
141
+ pendingRequests.set(requestId, { resolve, reject, timeout });
142
+ try {
143
+ w.stdin.write(JSON.stringify({ ...msg, requestId }) + '\n');
144
+ } catch (e) {
145
+ clearTimeout(timeout);
146
+ pendingRequests.delete(requestId);
147
+ reject(new Error(`Failed to write to worker: ${e.message}`));
148
+ }
118
149
  });
119
150
  }
120
151
 
@@ -78,6 +78,18 @@ export async function installAtlisp() {
78
78
  return { content: [{ type: 'text', text: '已发送 @lisp 安装代码到 CAD' }] };
79
79
  }
80
80
 
81
+ export async function listFunctionsInCad() {
82
+ if (!cad.connected) { await cad.connect(); }
83
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
84
+ try {
85
+ const result = await cad.sendCommandWithResult('(atoms-family 0)', null);
86
+ if (result) return { content: [{ type: 'text', text: result }] };
87
+ return { content: [{ type: 'text', text: 'CAD 函数列表为空' }] };
88
+ } catch (e) {
89
+ return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
90
+ }
91
+ }
92
+
81
93
  export async function initAtlisp() {
82
94
  if (!cad.connected) { await cad.connect(); }
83
95
  if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
@@ -1,9 +1,10 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { log } from '../logger.js';
4
+ import * as cadHandlers from './cad-handlers.js';
4
5
 
5
6
  const FUNCTIONS_DIR = process.env.FUNCTIONS_DIR || '';
6
- const FUNCTIONS_URL = process.env.FUNCTIONS_URL || 'https://s3.atlisp.cn/json/';
7
+ const FUNCTIONS_URL = process.env.FUNCTIONS_URL || 'http://s3.atlisp.cn/json/functions.json';
7
8
 
8
9
  async function fileExists(filePath) {
9
10
  try { await fs.access(filePath); return true; } catch { return false; }
@@ -13,40 +14,25 @@ export async function getFunctionUsage(funcName, packageName) {
13
14
  try {
14
15
  let results = [];
15
16
 
16
- const url = packageName
17
- ? `${FUNCTIONS_URL}${packageName}.json`
18
- : `${FUNCTIONS_URL}all.json`;
19
-
20
17
  try {
21
- const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
18
+ const response = await fetch(FUNCTIONS_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
22
19
  if (response.ok) {
23
20
  const data = await response.json();
24
- if (data.functions) {
25
- const func = data.functions.find(f => f.name === funcName);
26
- if (func) results.push(func);
27
- }
21
+ const funcs = data.functions || [];
22
+ const func = funcs.find(f => f.name === funcName || f.name === `${packageName}:${funcName}`);
23
+ if (func) results.push(func);
28
24
  }
29
25
  } catch (e) {
30
26
  log(`function-handlers fetch error: ${e.message}`);
31
27
  }
32
28
 
33
29
  if (results.length === 0 && FUNCTIONS_DIR && await fileExists(FUNCTIONS_DIR)) {
34
- if (packageName) {
35
- const filePath = path.join(FUNCTIONS_DIR, `${packageName}.json`);
36
- if (await fileExists(filePath)) {
37
- const raw = await fs.readFile(filePath, 'utf-8');
38
- const data = JSON.parse(raw);
39
- const func = data.functions?.find(f => f.name === funcName);
40
- if (func) results.push(func);
41
- }
42
- } else {
43
- const files = (await fs.readdir(FUNCTIONS_DIR)).filter(f => f.endsWith('.json'));
44
- for (const file of files) {
45
- const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
46
- const data = JSON.parse(raw);
47
- const func = data.functions?.find(f => f.name === funcName);
48
- if (func) results.push(func);
49
- }
30
+ const files = (await fs.readdir(FUNCTIONS_DIR)).filter(f => f.endsWith('.json'));
31
+ for (const file of files) {
32
+ const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
33
+ const data = JSON.parse(raw);
34
+ const func = data.functions?.find(f => f.name === funcName);
35
+ if (func) results.push(func);
50
36
  }
51
37
  }
52
38
 
@@ -71,18 +57,15 @@ export async function listFunctions(packageName) {
71
57
  try {
72
58
  let allFuncs = [];
73
59
 
74
- const url = packageName
75
- ? `${FUNCTIONS_URL}${packageName}.json`
76
- : `${FUNCTIONS_URL}all.json`;
77
-
78
60
  try {
79
- const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
61
+ const response = await fetch(FUNCTIONS_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
80
62
  if (response.ok) {
81
63
  const data = await response.json();
82
- if (data.functions) {
83
- allFuncs = packageName
84
- ? data.functions
85
- : data.functions.map(f => `${f.name}: ${f.description || '-'}`);
64
+ const funcs = data.functions || [];
65
+ if (packageName) {
66
+ allFuncs = funcs.filter(f => f.name.startsWith(`${packageName}:`));
67
+ } else {
68
+ allFuncs = funcs.map(f => `${f.name}: ${f.description || '-'}`);
86
69
  }
87
70
  }
88
71
  } catch (e) {
@@ -92,29 +75,32 @@ export async function listFunctions(packageName) {
92
75
  if (allFuncs.length === 0 && FUNCTIONS_DIR && await fileExists(FUNCTIONS_DIR)) {
93
76
  const files = (await fs.readdir(FUNCTIONS_DIR)).filter(f => f.endsWith('.json'));
94
77
  for (const file of files) {
95
- const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
96
- const data = JSON.parse(raw);
97
- if (packageName) {
98
- if (data.package === packageName) {
99
- allFuncs = data.functions || [];
100
- }
101
- } else {
102
- for (const f of (data.functions || [])) {
103
- allFuncs.push(`${f.name}: ${f.description || '-'}`);
78
+ try {
79
+ const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
80
+ const data = JSON.parse(raw);
81
+ const funcs = data.functions || [];
82
+ if (packageName) {
83
+ allFuncs = funcs.filter(f => f.name.startsWith(`${packageName}:`));
84
+ } else {
85
+ for (const f of funcs) {
86
+ allFuncs.push(`${f.name}: ${f.description || '-'}`);
87
+ }
104
88
  }
89
+ } catch (err) {
90
+ log(`read file ${file} error: ${err.message}`);
105
91
  }
106
92
  }
107
93
  }
108
94
 
109
95
  if (allFuncs.length === 0) {
110
- return { content: [{ type: 'text', text: '函数库为空' }] };
96
+ return await cadHandlers.listFunctionsInCad();
111
97
  }
112
98
 
113
99
  if (packageName) {
114
100
  return { content: [{ type: 'text', text: `${packageName} 包函数 (${allFuncs.length}):\n${allFuncs.map(f => f.name).join('\n')}` }] };
115
101
  }
116
102
 
117
- return { content: [{ type: 'text', text: `函数库共 ${allFuncs.length} 个函数` }] };
103
+ return { content: [{ type: 'text', text: allFuncs.join('\n') }] };
118
104
  } catch (e) {
119
105
  return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
120
106
  }
@@ -1,17 +1,57 @@
1
+ const PACKAGES_LIST_URL = process.env.PACKAGES_LIST_URL || 'http://s3.atlisp.cn/packages-list?fmt=json';
1
2
  import { cad } from '../cad.js';
2
3
  import { MOCK_PACKAGES } from '../constants.js';
4
+ let cachedPackages = null;
5
+ let cachedAt = null;
6
+ const CACHE_TTL = 10 * 60 * 1000;
7
+
8
+ async function fetchPackages() {
9
+ const now = Date.now();
10
+ if (cachedPackages && cachedAt && (now - cachedAt) < CACHE_TTL) {
11
+ return cachedPackages;
12
+ }
13
+ try {
14
+ const response = await fetch(PACKAGES_LIST_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
15
+ if (response.ok) {
16
+ const data = await response.json();
17
+ cachedPackages = data;
18
+ cachedAt = now;
19
+ return data;
20
+ }
21
+ } catch (e) {}
22
+ return null;
23
+ }
3
24
 
4
25
  export async function listPackages() {
26
+ if (!cad.connected) await cad.connect();
27
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
28
+ const result = await cad.sendCommandWithResult('(@::package-list-in-local)');
29
+ if (result && result !== 'nil') return { content: [{ type: 'text', text: result }] };
5
30
  const packages = MOCK_PACKAGES.map(p => `${p.name} v${p.version} - ${p.description}`);
6
31
  return { content: [{ type: 'text', text: `已安装的 @lisp 包:\n${packages.join('\n')}` }] };
7
32
  }
8
33
 
9
34
  export async function searchPackages(query) {
10
- const results = MOCK_PACKAGES.filter(p => p.name.includes(query) || p.description.includes(query));
35
+ const data = await fetchPackages();
36
+ if (!data) {
37
+ return { content: [{ type: 'text', text: '无法获取包列表' }], isError: true };
38
+ }
39
+ const results = [];
40
+ const items = Array.isArray(data) ? data : (data.packages || []);
41
+ if (query) {
42
+ const q = query.toLowerCase();
43
+ for (const p of items) {
44
+ const name = (p.name || '').toLowerCase();
45
+ const desc = (p.description || '').toLowerCase();
46
+ if (name.includes(q) || desc.includes(q)) results.push(p);
47
+ }
48
+ } else {
49
+ for (const p of items) results.push(p);
50
+ }
11
51
  if (results.length === 0) {
12
52
  return { content: [{ type: 'text', text: `未找到包含 "${query}" 的包` }] };
13
53
  }
14
- return { content: [{ type: 'text', text: `搜索结果:\n${results.map(p => `${p.name} v${p.version} - ${p.description}`).join('\n')}` }] };
54
+ return { content: [{ type: 'text', text: `搜索结果 (${results.length}):\n${results.map(p => `${p.name} v${p.version || ''} - ${p.description || ''}`).join('\n')}` }] };
15
55
  }
16
56
 
17
57
  export async function installPackage(packageName) {
@@ -0,0 +1,89 @@
1
+ export class Session {
2
+ constructor(clientId) {
3
+ this.clientId = clientId;
4
+ this.subscriptions = new Set();
5
+ this.createdAt = Date.now();
6
+ this.connected = false;
7
+ }
8
+ }
9
+
10
+ export class SessionManager {
11
+ #sessions = new Map();
12
+
13
+ createSession(clientId) {
14
+ let session = this.#sessions.get(clientId);
15
+ if (!session) {
16
+ session = new Session(clientId);
17
+ this.#sessions.set(clientId, session);
18
+ }
19
+ return session;
20
+ }
21
+
22
+ getSession(clientId) {
23
+ return this.#sessions.get(clientId) || null;
24
+ }
25
+
26
+ hasSession(clientId) {
27
+ return this.#sessions.has(clientId);
28
+ }
29
+
30
+ removeSession(clientId) {
31
+ return this.#sessions.delete(clientId);
32
+ }
33
+
34
+ subscribe(clientId, uri) {
35
+ const session = this.#sessions.get(clientId);
36
+ if (session) {
37
+ session.subscriptions.add(uri);
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ unsubscribe(clientId, uri) {
44
+ const session = this.#sessions.get(clientId);
45
+ if (session) {
46
+ return session.subscriptions.delete(uri);
47
+ }
48
+ return false;
49
+ }
50
+
51
+ getSubscriptions(clientId) {
52
+ const session = this.#sessions.get(clientId);
53
+ return session ? Array.from(session.subscriptions) : [];
54
+ }
55
+
56
+ isSubscribed(clientId, uri) {
57
+ const session = this.#sessions.get(clientId);
58
+ return session ? session.subscriptions.has(uri) : false;
59
+ }
60
+
61
+ getAllSubscribedUris() {
62
+ const all = new Set();
63
+ for (const session of this.#sessions.values()) {
64
+ for (const uri of session.subscriptions) {
65
+ all.add(uri);
66
+ }
67
+ }
68
+ return all;
69
+ }
70
+
71
+ list() {
72
+ return Array.from(this.#sessions.entries()).map(([id, s]) => ({
73
+ clientId: id,
74
+ subscriptions: Array.from(s.subscriptions),
75
+ createdAt: s.createdAt,
76
+ connected: s.connected
77
+ }));
78
+ }
79
+
80
+ count() {
81
+ return this.#sessions.size;
82
+ }
83
+
84
+ clear() {
85
+ this.#sessions.clear();
86
+ }
87
+ }
88
+
89
+ export default SessionManager;
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ const SESSION_TIMEOUT = 30 * 60 * 1000;
4
+
5
+ export class SessionTransport {
6
+ constructor(sessionId) {
7
+ this.sessionId = sessionId || randomUUID();
8
+ this.onclose = null;
9
+ this.onerror = null;
10
+ this.onmessage = null;
11
+ this._pendingRequest = null;
12
+ this._started = false;
13
+ this._closed = false;
14
+ this._cleanupTimer = null;
15
+ }
16
+
17
+ async start() {
18
+ if (this._started) {
19
+ throw new Error('Transport already started');
20
+ }
21
+ this._started = true;
22
+ }
23
+
24
+ async send(message) {
25
+ if (this._closed) return;
26
+ if (this._pendingRequest) {
27
+ const { resolve, reject, timeout } = this._pendingRequest;
28
+ clearTimeout(timeout);
29
+ this._pendingRequest = null;
30
+ resolve(message);
31
+ }
32
+ }
33
+
34
+ async close() {
35
+ if (this._closed) return;
36
+ this._closed = true;
37
+ if (this._pendingRequest) {
38
+ const { reject, timeout } = this._pendingRequest;
39
+ clearTimeout(timeout);
40
+ this._pendingRequest = null;
41
+ reject(new Error('Transport closed'));
42
+ }
43
+ this._clearCleanupTimer();
44
+ this.onclose?.();
45
+ }
46
+
47
+ async handleRequest(message, timeoutMs = 60000) {
48
+ if (this._closed) {
49
+ throw new Error('Transport closed');
50
+ }
51
+
52
+ if (!message || !message.method) {
53
+ throw new Error('Invalid message');
54
+ }
55
+
56
+ if (!message.id) {
57
+ this.onmessage?.(message);
58
+ return null;
59
+ }
60
+
61
+ return new Promise((resolve, reject) => {
62
+ const timeout = setTimeout(() => {
63
+ if (this._pendingRequest?.resolve === resolve) {
64
+ this._pendingRequest = null;
65
+ reject(new Error('Request timeout'));
66
+ }
67
+ }, timeoutMs);
68
+
69
+ this._pendingRequest = { resolve, reject, timeout };
70
+ this.onmessage?.(message);
71
+ });
72
+ }
73
+
74
+ resetCleanupTimer() {
75
+ this._clearCleanupTimer();
76
+ this._cleanupTimer = setTimeout(() => {
77
+ this.close();
78
+ }, SESSION_TIMEOUT);
79
+ }
80
+
81
+ _clearCleanupTimer() {
82
+ if (this._cleanupTimer) {
83
+ clearTimeout(this._cleanupTimer);
84
+ this._cleanupTimer = null;
85
+ }
86
+ }
87
+ }
88
+
89
+ export default SessionTransport;
@@ -1,25 +1,38 @@
1
- const subscriptions = new Map();
1
+ import { SessionManager } from './session-manager.js';
2
+
3
+ const sessionManager = new SessionManager();
2
4
  let _notifyFn = null;
3
5
 
4
6
  export function setNotify(fn) {
5
7
  _notifyFn = fn;
6
8
  }
7
9
 
8
- export function subscribe(uri) {
9
- subscriptions.set(uri, Date.now());
10
+ export function subscribe(uri, sessionId) {
11
+ if (sessionId) {
12
+ return sessionManager.subscribe(sessionId, uri);
13
+ }
10
14
  return true;
11
15
  }
12
16
 
13
- export function unsubscribe(uri) {
14
- return subscriptions.delete(uri);
17
+ export function unsubscribe(uri, sessionId) {
18
+ if (sessionId) {
19
+ return sessionManager.unsubscribe(sessionId, uri);
20
+ }
21
+ return true;
15
22
  }
16
23
 
17
- export function list() {
18
- return Array.from(subscriptions.keys());
24
+ export function list(sessionId) {
25
+ if (sessionId) {
26
+ return sessionManager.getSubscriptions(sessionId);
27
+ }
28
+ return Array.from(sessionManager.getAllSubscribedUris());
19
29
  }
20
30
 
21
- export function isSubscribed(uri) {
22
- return subscriptions.has(uri);
31
+ export function isSubscribed(uri, sessionId) {
32
+ if (sessionId) {
33
+ return sessionManager.isSubscribed(sessionId, uri);
34
+ }
35
+ return sessionManager.getAllSubscribedUris().has(uri);
23
36
  }
24
37
 
25
38
  export function notify(uri, data) {
@@ -29,9 +42,11 @@ export function notify(uri, data) {
29
42
  }
30
43
 
31
44
  export function clear() {
32
- subscriptions.clear();
45
+ sessionManager.clear();
33
46
  }
34
47
 
35
48
  export function size() {
36
- return subscriptions.size;
37
- }
49
+ return sessionManager.count();
50
+ }
51
+
52
+ export { sessionManager };