@atlisp/mcp 1.0.19 → 1.0.20

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/src/atlisp-mcp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import express from 'express';
2
2
  import rawBody from 'raw-body';
3
3
  import iconv from 'iconv-lite';
4
+ import rateLimit from 'express-rate-limit';
4
5
  const { decode: charsetDecode, encodingExists } = iconv;
5
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -11,28 +12,58 @@ import {
11
12
  ReadResourceRequestSchema,
12
13
  SubscribeRequestSchema,
13
14
  UnsubscribeRequestSchema,
15
+ ListPromptsRequestSchema,
16
+ GetPromptRequestSchema,
14
17
  } from '@modelcontextprotocol/sdk/types.js';
15
18
  import { cad } from './cad.js';
16
19
  import { CAD_PLATFORMS, FILE_EXTENSIONS, PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION } from './constants.js';
17
- import { log } from './logger.js';
20
+ import { log, closeLog } from './logger.js';
18
21
  import * as cadHandlers from './handlers/cad-handlers.js';
19
22
  import * as packageHandlers from './handlers/package-handlers.js';
20
23
  import * as functionHandlers from './handlers/function-handlers.js';
21
24
  import { listResources, readResource } from './handlers/resource-handlers.js';
25
+ import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
22
26
  import * as subMgr from './subscription-manager.js';
23
27
  import { SseSession, SSE_EVENTS, createSseResponseHandler, parseLastEventId } from './sse-session.js';
24
28
  import { SseSessionManager } from './sse-session-manager.js';
29
+ import { createError, toMcpError } from './errors.js';
30
+ import config from './config.js';
25
31
 
26
- const PORT = process.env.PORT || 8110;
27
- const HOST = process.env.HOST || '0.0.0.0';
28
- const TRANSPORT = process.env.TRANSPORT || 'http';
29
- const API_KEY = process.env.MCP_API_KEY || '';
30
- const ENABLE_SSE = process.env.ENABLE_SSE === 'true';
31
- if (API_KEY) log('API Key authentication enabled');
32
- if (ENABLE_SSE) log('SSE mode enabled');
32
+ const SERVER_CAPABILITIES = {
33
+ tools: {},
34
+ resources: { subscribe: true, listChanged: true },
35
+ prompts: {}
36
+ };
37
+
38
+ const SERVER_INFO = {
39
+ protocolVersion: PROTOCOL_VERSION,
40
+ capabilities: SERVER_CAPABILITIES,
41
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
42
+ };
43
+
44
+ const { port, host, transport, apiKey, enableSse, rateLimitWindow, rateLimitMax } = config;
45
+
46
+ if (apiKey) log('API Key authentication enabled');
47
+ if (enableSse) log('SSE mode enabled');
48
+
49
+ const mcpLimiter = rateLimit({
50
+ windowMs: rateLimitWindow,
51
+ max: rateLimitMax,
52
+ standardHeaders: true,
53
+ legacyHeaders: false,
54
+ message: { error: '请求过于频繁,请稍后再试' }
55
+ });
33
56
 
34
57
  const sseSessionManager = new SseSessionManager();
35
58
 
59
+ subMgr.setNotify((uri, data) => {
60
+ sseSessionManager.broadcast(SSE_EVENTS.MESSAGE, {
61
+ jsonrpc: '2.0',
62
+ method: 'notifications/resources/updated',
63
+ params: { uri, data }
64
+ });
65
+ });
66
+
36
67
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
37
68
 
38
69
  const TOOL_HANDLERS = {
@@ -183,7 +214,31 @@ export const tools = [
183
214
  export async function handleToolCall(name, args) {
184
215
  const handler = TOOL_HANDLERS[name];
185
216
  if (!handler) return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
186
- return handler(args);
217
+ try {
218
+ const result = await handler(args || {});
219
+ notifyResourceChanges(name);
220
+ return result;
221
+ } catch (e) {
222
+ log(`Tool call error [${name}]: ${e.message}`);
223
+ return { content: [{ type: 'text', text: `工具执行错误: ${e.message}` }], isError: true };
224
+ }
225
+ }
226
+
227
+ function notifyResourceChanges(toolName) {
228
+ const resourceMap = {
229
+ connect_cad: ['atlisp://cad/info'],
230
+ new_document: ['atlisp://dwg/name', 'atlisp://dwg/path', 'atlisp://cad/layers', 'atlisp://cad/entities'],
231
+ install_package: ['atlisp://packages'],
232
+ install_atlisp: ['atlisp://packages'],
233
+ };
234
+ const uris = resourceMap[toolName];
235
+ if (uris) {
236
+ for (const uri of uris) {
237
+ if (subMgr.isSubscribed(uri)) {
238
+ subMgr.notify(uri, null);
239
+ }
240
+ }
241
+ }
187
242
  }
188
243
 
189
244
  async function initCadConnection() {
@@ -206,7 +261,7 @@ let server = null;
206
261
  export function createMcpServer() {
207
262
  server = new Server(
208
263
  { name: SERVER_NAME, version: SERVER_VERSION },
209
- { capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } } }
264
+ { capabilities: SERVER_CAPABILITIES }
210
265
  );
211
266
 
212
267
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -252,88 +307,82 @@ export function createMcpServer() {
252
307
  return { success: true };
253
308
  });
254
309
 
310
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
311
+ prompts: await listPrompts()
312
+ }));
313
+
314
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
315
+ const { name, arguments: args } = request.params;
316
+ return await getPrompt(name, args || {});
317
+ });
318
+
255
319
  return server;
256
320
  }
257
321
 
258
- async function handleMcpRequest(session, method, params, id) {
322
+ async function dispatchMcpMethod(responder, method, params, id) {
259
323
  try {
260
324
  if (method === 'initialize') {
261
- session.sendResponse({ jsonrpc: '2.0', id, result: {
262
- protocolVersion: PROTOCOL_VERSION,
263
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
264
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
265
- } });
325
+ responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
266
326
  return;
267
327
  }
268
328
 
269
- if (method === 'tools/list') {
270
- session.sendResponse({ jsonrpc: '2.0', id, result: { tools } });
271
- return;
272
- }
273
-
274
- if (method === 'resources/list') {
275
- const resources = await listResources(subMgr.list());
276
- session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
277
- return;
278
- }
279
-
280
- if (method === 'resources/read') {
281
- const uri = params?.uri;
282
- if (!uri) {
283
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
284
- return;
285
- }
286
- try {
329
+ const methodHandlers = {
330
+ 'tools/list': async () => ({ tools }),
331
+ 'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
332
+ 'resources/read': async () => {
333
+ const uri = params?.uri;
334
+ if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
287
335
  const data = await readResource(uri);
288
- session.sendResponse({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } });
289
- } catch (e) {
290
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
291
- }
292
- return;
293
- }
294
-
295
- if (method === 'resources/subscribe') {
296
- const uri = params?.uri;
297
- if (!uri) {
298
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
299
- return;
336
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
337
+ },
338
+ 'resources/subscribe': async () => {
339
+ const uri = params?.uri;
340
+ if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
341
+ subMgr.subscribe(uri);
342
+ return { success: true };
343
+ },
344
+ 'resources/unsubscribe': async () => {
345
+ const uri = params?.uri;
346
+ if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
347
+ subMgr.unsubscribe(uri);
348
+ return { success: true };
349
+ },
350
+ 'tools/call': async () => {
351
+ const toolName = params?.name;
352
+ if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
353
+ return await handleToolCall(toolName, params?.arguments || {});
354
+ },
355
+ 'prompts/list': async () => ({ prompts: await listPrompts() }),
356
+ 'prompts/get': async () => {
357
+ const promptName = params?.name;
358
+ if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
359
+ return await getPrompt(promptName, params?.arguments || {});
300
360
  }
301
- subMgr.subscribe(uri);
302
- session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
303
- return;
361
+ };
362
+
363
+ const handler = methodHandlers[method];
364
+ if (handler) {
365
+ const result = await handler();
366
+ responder({ jsonrpc: '2.0', id, result });
367
+ } else {
368
+ responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
304
369
  }
305
-
306
- if (method === 'resources/unsubscribe') {
307
- const uri = params?.uri;
308
- if (!uri) {
309
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
310
- return;
311
- }
312
- subMgr.unsubscribe(uri);
313
- session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
314
- return;
315
- }
316
-
317
- if (method === 'tools/call') {
318
- const toolName = params?.name;
319
- if (!toolName) {
320
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
321
- return;
322
- }
323
- const result = await handleToolCall(toolName, params?.arguments || {});
324
- session.sendResponse({ jsonrpc: '2.0', id, result });
325
- return;
326
- }
327
-
328
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
329
370
  } catch (e) {
330
- log('MCP error: ' + e.message);
331
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
371
+ const mcpError = toMcpError(e);
372
+ log(`MCP error [${method}]: ${mcpError.message}`);
373
+ responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
332
374
  }
333
375
  }
334
376
 
377
+ async function handleMcpRequest(session, method, params, id) {
378
+ await dispatchMcpMethod(
379
+ (response) => session.sendResponse(response),
380
+ method, params, id
381
+ );
382
+ }
383
+
335
384
  export async function startServer() {
336
- if (TRANSPORT === 'stdio') {
385
+ if (transport === 'stdio') {
337
386
  await initCadConnection();
338
387
  const mcpServer = createMcpServer();
339
388
  const transport = new StdioServerTransport();
@@ -366,11 +415,11 @@ export async function startServer() {
366
415
  });
367
416
 
368
417
  const PUBLIC_PATHS = ['/health'];
369
- if (API_KEY) {
418
+ if (apiKey) {
370
419
  app.use((req, res, next) => {
371
420
  if (PUBLIC_PATHS.includes(req.path)) return next();
372
421
  const auth = req.get('Authorization');
373
- if (auth === `Bearer ${API_KEY}`) return next();
422
+ if (auth === `Bearer ${apiKey}`) return next();
374
423
  res.status(401).json({ error: 'Unauthorized' });
375
424
  });
376
425
  }
@@ -379,11 +428,33 @@ export async function startServer() {
379
428
  res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
380
429
  });
381
430
 
382
- if (process.env.ENABLE_CORS) {
431
+ app.use((req, res, next) => {
432
+ const path = req.path;
433
+ if (path.startsWith('/%7B') || path.startsWith('/{"')) {
434
+ try {
435
+ const decoded = decodeURIComponent(path.slice(1));
436
+ if (decoded.startsWith('{"path":')) {
437
+ const data = JSON.parse(decoded);
438
+ const endpoint = data.path;
439
+ log(`Client sent SSE endpoint in URL path, extracting: ${endpoint}`);
440
+ req.url = endpoint + ((req._parsedUrl && req._parsedUrl.search) || '');
441
+ try { req.path = endpoint; } catch (_) { /* Express 5: req.path is read-only, req.url is sufficient */ }
442
+ }
443
+ } catch (e) {
444
+ log(`Failed to parse malformed path: ${e.message}`);
445
+ }
446
+ }
447
+ next();
448
+ });
449
+
450
+ if (config.enableCors) {
383
451
  app.use((req, res, next) => {
384
- res.setHeader('Access-Control-Allow-Origin', process.env.CORS_ORIGIN || '*');
385
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
386
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, mcp-session-id');
452
+ res.setHeader('Access-Control-Allow-Origin', config.corsOrigin);
453
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, CONNECT');
454
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, mcp-session-id, last-event-id');
455
+ res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id, content-type, x-accel-buffering');
456
+ res.setHeader('Access-Control-Max-Age', '86400');
457
+ res.setHeader('Vary', 'Accept');
387
458
  if (req.method === 'OPTIONS') return res.status(204).end();
388
459
  next();
389
460
  });
@@ -391,47 +462,108 @@ export async function startServer() {
391
462
 
392
463
  createMcpServer();
393
464
 
394
- if (ENABLE_SSE) {
395
- app.get('/sse', (req, res) => {
396
- const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
397
-
398
- const session = new SseSession(res, { clientId });
399
-
400
- session.sendEndpoint('/mcp');
401
- session.sendCapabilities({
402
- protocolVersion: PROTOCOL_VERSION,
403
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
404
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
405
- });
406
-
465
+ function setupSseConnection(req, res) {
466
+ log(`SSE connect: ${req.method} ${req.path} - session: ${req.get('mcp-session-id') || 'new'}`);
467
+ const sessionId = req.get('mcp-session-id');
468
+ const lastEventId = parseLastEventId(req);
469
+ const clientId = sessionId || crypto.randomUUID();
470
+
471
+ res.setHeader('mcp-session-id', clientId);
472
+
473
+ let session;
474
+ if (sessionId && sseSessionManager.has(sessionId)) {
475
+ session = sseSessionManager.get(sessionId);
476
+ } else {
477
+ session = new SseSession(res, { clientId, resumeFromId: lastEventId });
407
478
  sseSessionManager.add(clientId, session);
408
-
409
- res.on('close', () => {
410
- sseSessionManager.remove(clientId);
411
- });
479
+ }
480
+
481
+ if (lastEventId && lastEventId > 0) {
482
+ session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
483
+ }
484
+
485
+ session.sendPing();
486
+
487
+ res.on('close', () => {
488
+ log(`SSE close: session ${clientId}`);
489
+ sseSessionManager.remove(clientId);
412
490
  });
491
+ }
413
492
 
414
- ['/mcp/sse'].forEach(p => app.all(p, (req, res) => {
415
- const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
416
-
417
- const session = new SseSession(res, { clientId });
418
-
419
- session.sendEndpoint('/mcp');
420
- session.sendCapabilities({
421
- protocolVersion: PROTOCOL_VERSION,
422
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
423
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
424
- });
425
-
426
- sseSessionManager.add(clientId, session);
427
-
428
- res.on('close', () => {
429
- sseSessionManager.remove(clientId);
430
- });
431
- }));
493
+ if (enableSse) {
494
+ app.get('/sse', (req, res) => setupSseConnection(req, res));
495
+ app.get('/mcp/sse', (req, res) => setupSseConnection(req, res));
496
+
497
+ app.post('/mcp/message', mcpLimiter, async (req, res) => {
498
+ const sessionId = req.get('mcp-session-id') || '';
499
+ const accept = req.get('Accept') || '';
500
+ const wantsSse = accept.includes('text/event-stream');
501
+ log(`POST /mcp/message - session: ${sessionId || 'new'} - wantsSSE: ${wantsSse}`);
502
+
503
+ let session;
504
+ if (sessionId && sseSessionManager.has(sessionId)) {
505
+ session = sseSessionManager.get(sessionId);
506
+ } else if (wantsSse) {
507
+ const clientId = sessionId || crypto.randomUUID();
508
+ res.setHeader('mcp-session-id', clientId);
509
+ session = new SseSession(res, { clientId });
510
+ sseSessionManager.add(clientId, session);
511
+ log(`POST /mcp/message - created new session: ${clientId}`);
512
+ res.on('close', () => {
513
+ log(`POST SSE close: session ${clientId}`);
514
+ sseSessionManager.remove(clientId);
515
+ });
516
+ }
517
+
518
+ const body = req.body || {};
519
+ const { method, params, id } = body;
520
+
521
+ if (!id) {
522
+ if (session) res.status(204).end();
523
+ else res.status(202).end();
524
+ return;
525
+ }
526
+
527
+ if (session) {
528
+ try {
529
+ await handleMcpRequest(session, method, params, id);
530
+ } catch (e) {
531
+ log('SSE message error: ' + e.message);
532
+ session.sendError(-32603, e.message, id);
533
+ }
534
+ res.status(204).end();
535
+ } else {
536
+ await dispatchMcpMethod(
537
+ (response) => {
538
+ res.setHeader('Content-Type', 'application/json');
539
+ res.status(200).json(response);
540
+ },
541
+ method, params, id
542
+ );
543
+ }
544
+ });
432
545
  }
433
546
 
434
- app.post('/mcp', async (req, res) => {
547
+ app.get('/mcp', (req, res) => {
548
+ const accept = req.get('Accept') || '';
549
+ if (accept.includes('text/event-stream')) {
550
+ if (!enableSse) {
551
+ res.status(400).json({ error: 'SSE mode is disabled' });
552
+ return;
553
+ }
554
+ setupSseConnection(req, res);
555
+ return;
556
+ }
557
+ res.status(200).json({
558
+ name: SERVER_NAME,
559
+ version: SERVER_VERSION,
560
+ transport: 'streamable-http',
561
+ sseEndpoint: '/sse',
562
+ messageEndpoint: '/mcp/message'
563
+ });
564
+ });
565
+
566
+ app.post('/mcp', mcpLimiter, async (req, res) => {
435
567
  const body = req.body || {};
436
568
  const { method, params, id } = body;
437
569
  const accept = req.get('Accept') || '';
@@ -440,110 +572,62 @@ export async function startServer() {
440
572
  log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
441
573
 
442
574
  if (wantsSse) {
443
- if (!ENABLE_SSE) {
575
+ if (!enableSse) {
444
576
  res.status(400).json({ error: 'SSE mode is disabled' });
445
577
  return;
446
578
  }
447
- res.setHeader('Content-Type', 'text/event-stream');
448
- res.setHeader('Cache-Control', 'no-cache');
449
- res.setHeader('Connection', 'keep-alive');
450
-
579
+
580
+ const clientId = sessionId || crypto.randomUUID();
581
+ res.setHeader('mcp-session-id', clientId);
582
+
583
+ let session;
451
584
  if (sessionId && sseSessionManager.has(sessionId)) {
452
- const session = sseSessionManager.get(sessionId);
585
+ session = sseSessionManager.get(sessionId);
453
586
  if (!id) {
454
587
  res.status(204).end();
455
588
  return;
456
589
  }
457
590
  await handleMcpRequest(session, method, params, id);
458
- return;
459
- }
460
- }
461
-
462
- res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
463
- const { send } = createSseResponseHandler(res, wantsSse);
464
-
465
- if (!id) return res.status(204).end();
466
-
467
- try {
468
- if (method === 'initialize') {
469
- send({ jsonrpc: '2.0', id, result: {
470
- protocolVersion: PROTOCOL_VERSION,
471
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
472
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
473
- } }, true);
591
+ res.status(204).end();
474
592
  return;
475
593
  }
476
594
 
477
- if (method === 'tools/list') {
478
- send({ jsonrpc: '2.0', id, result: { tools } }, true);
479
- return;
480
- }
481
-
482
- if (method === 'resources/list') {
483
- const resources = await listResources(subMgr.list());
484
- send({ jsonrpc: '2.0', id, result: { resources } }, true);
485
- return;
486
- }
487
-
488
- if (method === 'resources/read') {
489
- const uri = params?.uri;
490
- if (!uri) {
491
- send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
492
- return;
493
- }
494
- try {
495
- const data = await readResource(uri);
496
- send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
497
- } catch (e) {
498
- send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
499
- }
500
- return;
501
- }
595
+ session = new SseSession(res, { clientId });
596
+ sseSessionManager.add(clientId, session);
502
597
 
503
- if (method === 'resources/subscribe') {
504
- const uri = params?.uri;
505
- if (!uri) {
506
- send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
507
- return;
508
- }
509
- subMgr.subscribe(uri);
510
- send({ jsonrpc: '2.0', id, result: { success: true } }, true);
598
+ if (!id) {
511
599
  return;
512
600
  }
601
+ await handleMcpRequest(session, method, params, id);
602
+ return;
603
+ }
513
604
 
514
- if (method === 'resources/unsubscribe') {
515
- const uri = params?.uri;
516
- if (!uri) {
517
- send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
518
- return;
519
- }
520
- subMgr.unsubscribe(uri);
521
- send({ jsonrpc: '2.0', id, result: { success: true } }, true);
522
- return;
523
- }
605
+ res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
606
+ const { send } = createSseResponseHandler(res, wantsSse);
524
607
 
525
- if (method === 'tools/call') {
526
- const toolName = params?.name;
527
- if (!toolName) {
528
- send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
529
- return;
530
- }
531
- const result = await handleToolCall(toolName, params?.arguments || {});
532
- send({ jsonrpc: '2.0', id, result }, true);
533
- return;
534
- }
608
+ if (!id) return res.status(204).end();
535
609
 
536
- send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
537
- } catch (e) {
538
- log('MCP error: ' + e.message);
539
- send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
540
- }
610
+ await dispatchMcpMethod(
611
+ (response) => send(response, true),
612
+ method, params, id
613
+ );
541
614
  });
542
615
 
543
- app.listen(PORT, HOST, async () => {
616
+ const httpServer = app.listen(config.port, config.host, async () => {
544
617
  await initCadConnection();
545
- console.error(`@lisp MCP Server 启动 - http://${HOST}:${PORT} - Streamable HTTP`);
618
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP`);
546
619
  });
620
+
621
+ async function shutdown(signal) {
622
+ console.error(`\n收到 ${signal},正在优雅关闭...`);
623
+ sseSessionManager.closeAll();
624
+ await cad.disconnect();
625
+ closeLog();
626
+ httpServer.close(() => process.exit(0));
627
+ setTimeout(() => process.exit(1), 5000);
628
+ }
629
+ process.on('SIGINT', () => shutdown('SIGINT'));
630
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
547
631
  }
548
632
  }
549
633
 
package/src/cad-worker.js CHANGED
@@ -11,7 +11,9 @@ const DEBUG_FILE = process.env.DEBUG_FILE || path.join(process.env.TEMP || '/tmp
11
11
 
12
12
  function log(msg) {
13
13
  const entry = `${new Date().toISOString()} ${msg}\n`;
14
- fs.appendFileSync(DEBUG_FILE, entry);
14
+ if (process.env.DEBUG) {
15
+ fs.appendFile(DEBUG_FILE, entry, () => {});
16
+ }
15
17
  }
16
18
 
17
19
  const cadConnect = edge.func(function() {/*
@@ -221,10 +223,13 @@ const cadSendWithResult = edge.func(function() {/*
221
223
 
222
224
  process.stdin.setEncoding('utf8');
223
225
 
226
+ let stdinBuffer = '';
224
227
  process.stdin.on('data', (data) => {
225
- const lines = data.trim().split('\n');
228
+ stdinBuffer += data;
229
+ const lines = stdinBuffer.split('\n');
230
+ stdinBuffer = lines.pop() || '';
226
231
  for (const line of lines) {
227
- if (!line) continue;
232
+ if (!line.trim()) continue;
228
233
  try {
229
234
  const msg = JSON.parse(line);
230
235
  handleMessage(msg).then(result => {
@@ -374,7 +379,9 @@ async function handleMessage(msg) {
374
379
  });
375
380
  });
376
381
  if (result && result.success) return result;
377
- } catch (e) {}
382
+ } catch (e) {
383
+ log('cadConnect failed: ' + e.message);
384
+ }
378
385
  return new Promise((resolve, reject) => {
379
386
  cadLaunch({}, (e, r) => {
380
387
  if (e) reject(e);