@atlisp/mcp 1.0.18 → 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,32 +12,64 @@ 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
- if (API_KEY) log('API Key authentication 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
+ });
31
56
 
32
57
  const sseSessionManager = new SseSessionManager();
33
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
+
34
67
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
35
68
 
36
69
  const TOOL_HANDLERS = {
37
70
  connect_cad: () => cadHandlers.connectCad(),
38
71
  eval_lisp: (a) => cadHandlers.evalLisp(a.code),
39
- eval_lisp_with_result: (a) => cadHandlers.evalLisp(a.code, true),
72
+ eval_lisp_with_result: (a) => cadHandlers.evalLisp(a.code, true, a.encoding),
40
73
  get_cad_info: () => cadHandlers.getCadInfo(),
41
74
  list_packages: () => packageHandlers.listPackages(),
42
75
  search_packages: (a) => packageHandlers.searchPackages(a.query),
@@ -76,7 +109,10 @@ export const tools = [
76
109
  description: '在 CAD 中执行 AutoLISP 代码并返回结果',
77
110
  inputSchema: {
78
111
  type: 'object',
79
- properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
112
+ properties: {
113
+ code: { type: 'string', description: '要执行的 LISP 代码' },
114
+ encoding: { type: 'string', description: '结果编码,如 utf-8, gbk, gb2312, gb18030(可选,默认自动检测)' }
115
+ },
80
116
  required: ['code']
81
117
  }
82
118
  },
@@ -178,7 +214,31 @@ export const tools = [
178
214
  export async function handleToolCall(name, args) {
179
215
  const handler = TOOL_HANDLERS[name];
180
216
  if (!handler) return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
181
- 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
+ }
182
242
  }
183
243
 
184
244
  async function initCadConnection() {
@@ -201,7 +261,7 @@ let server = null;
201
261
  export function createMcpServer() {
202
262
  server = new Server(
203
263
  { name: SERVER_NAME, version: SERVER_VERSION },
204
- { capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } } }
264
+ { capabilities: SERVER_CAPABILITIES }
205
265
  );
206
266
 
207
267
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -247,88 +307,82 @@ export function createMcpServer() {
247
307
  return { success: true };
248
308
  });
249
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
+
250
319
  return server;
251
320
  }
252
321
 
253
- async function handleMcpRequest(session, method, params, id) {
322
+ async function dispatchMcpMethod(responder, method, params, id) {
254
323
  try {
255
324
  if (method === 'initialize') {
256
- session.sendResponse({ jsonrpc: '2.0', id, result: {
257
- protocolVersion: PROTOCOL_VERSION,
258
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
259
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
260
- } });
325
+ responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
261
326
  return;
262
327
  }
263
328
 
264
- if (method === 'tools/list') {
265
- session.sendResponse({ jsonrpc: '2.0', id, result: { tools } });
266
- return;
267
- }
268
-
269
- if (method === 'resources/list') {
270
- const resources = await listResources(subMgr.list());
271
- session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
272
- return;
273
- }
274
-
275
- if (method === 'resources/read') {
276
- const uri = params?.uri;
277
- if (!uri) {
278
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
279
- return;
280
- }
281
- 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 参数' });
282
335
  const data = await readResource(uri);
283
- session.sendResponse({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } });
284
- } catch (e) {
285
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
286
- }
287
- return;
288
- }
289
-
290
- if (method === 'resources/subscribe') {
291
- const uri = params?.uri;
292
- if (!uri) {
293
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
294
- return;
295
- }
296
- subMgr.subscribe(uri);
297
- session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
298
- return;
299
- }
300
-
301
- if (method === 'resources/unsubscribe') {
302
- const uri = params?.uri;
303
- if (!uri) {
304
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
305
- return;
306
- }
307
- subMgr.unsubscribe(uri);
308
- session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
309
- return;
310
- }
311
-
312
- if (method === 'tools/call') {
313
- const toolName = params?.name;
314
- if (!toolName) {
315
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
316
- 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 || {});
317
360
  }
318
- const result = await handleToolCall(toolName, params?.arguments || {});
319
- session.sendResponse({ jsonrpc: '2.0', id, result });
320
- 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' } });
321
369
  }
322
-
323
- session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
324
370
  } catch (e) {
325
- log('MCP error: ' + e.message);
326
- 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 } });
327
374
  }
328
375
  }
329
376
 
377
+ async function handleMcpRequest(session, method, params, id) {
378
+ await dispatchMcpMethod(
379
+ (response) => session.sendResponse(response),
380
+ method, params, id
381
+ );
382
+ }
383
+
330
384
  export async function startServer() {
331
- if (TRANSPORT === 'stdio') {
385
+ if (transport === 'stdio') {
332
386
  await initCadConnection();
333
387
  const mcpServer = createMcpServer();
334
388
  const transport = new StdioServerTransport();
@@ -361,11 +415,11 @@ export async function startServer() {
361
415
  });
362
416
 
363
417
  const PUBLIC_PATHS = ['/health'];
364
- if (API_KEY) {
418
+ if (apiKey) {
365
419
  app.use((req, res, next) => {
366
420
  if (PUBLIC_PATHS.includes(req.path)) return next();
367
421
  const auth = req.get('Authorization');
368
- if (auth === `Bearer ${API_KEY}`) return next();
422
+ if (auth === `Bearer ${apiKey}`) return next();
369
423
  res.status(401).json({ error: 'Unauthorized' });
370
424
  });
371
425
  }
@@ -374,11 +428,33 @@ export async function startServer() {
374
428
  res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
375
429
  });
376
430
 
377
- 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) {
378
451
  app.use((req, res, next) => {
379
- res.setHeader('Access-Control-Allow-Origin', process.env.CORS_ORIGIN || '*');
380
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
381
- 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');
382
458
  if (req.method === 'OPTIONS') return res.status(204).end();
383
459
  next();
384
460
  });
@@ -386,153 +462,172 @@ export async function startServer() {
386
462
 
387
463
  createMcpServer();
388
464
 
389
- app.get('/sse', (req, res) => {
390
- const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
391
-
392
- const session = new SseSession(res, { clientId });
393
-
394
- session.sendEndpoint('/mcp');
395
- session.sendCapabilities({
396
- protocolVersion: PROTOCOL_VERSION,
397
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
398
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
399
- });
400
-
401
- sseSessionManager.add(clientId, session);
402
-
403
- res.on('close', () => {
404
- sseSessionManager.remove(clientId);
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();
407
470
 
408
- ['/mcp/sse'].forEach(p => app.all(p, (req, res) => {
409
- const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
410
-
411
- const session = new SseSession(res, { clientId });
412
-
413
- session.sendEndpoint('/mcp');
414
- session.sendCapabilities({
415
- protocolVersion: PROTOCOL_VERSION,
416
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
417
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
418
- });
419
-
420
- sseSessionManager.add(clientId, session);
421
-
422
- res.on('close', () => {
423
- sseSessionManager.remove(clientId);
424
- });
425
- }));
471
+ res.setHeader('mcp-session-id', clientId);
426
472
 
427
- app.post('/mcp', async (req, res) => {
428
- const body = req.body || {};
429
- const { method, params, id } = body;
430
- const accept = req.get('Accept') || '';
431
- const wantsSse = accept.includes('text/event-stream');
432
- const sessionId = req.get('mcp-session-id') || '';
433
- log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
473
+ let session;
474
+ if (sessionId && sseSessionManager.has(sessionId)) {
475
+ session = sseSessionManager.get(sessionId);
476
+ } else {
477
+ session = new SseSession(res, { clientId, resumeFromId: lastEventId });
478
+ sseSessionManager.add(clientId, session);
479
+ }
434
480
 
435
- if (wantsSse) {
436
- res.setHeader('Content-Type', 'text/event-stream');
437
- res.setHeader('Cache-Control', 'no-cache');
438
- res.setHeader('Connection', 'keep-alive');
439
-
440
- if (sessionId && sseSessionManager.has(sessionId)) {
441
- const session = sseSessionManager.get(sessionId);
442
- if (!id) {
443
- res.status(204).end();
444
- return;
445
- }
446
- await handleMcpRequest(session, method, params, id);
447
- return;
448
- }
481
+ if (lastEventId && lastEventId > 0) {
482
+ session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
449
483
  }
450
484
 
451
- res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
452
- const { send } = createSseResponseHandler(res, wantsSse);
485
+ session.sendPing();
453
486
 
454
- if (!id) return res.status(204).end();
487
+ res.on('close', () => {
488
+ log(`SSE close: session ${clientId}`);
489
+ sseSessionManager.remove(clientId);
490
+ });
491
+ }
455
492
 
456
- try {
457
- if (method === 'initialize') {
458
- send({ jsonrpc: '2.0', id, result: {
459
- protocolVersion: PROTOCOL_VERSION,
460
- capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
461
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
462
- } }, true);
463
- return;
464
- }
493
+ if (enableSse) {
494
+ app.get('/sse', (req, res) => setupSseConnection(req, res));
495
+ app.get('/mcp/sse', (req, res) => setupSseConnection(req, res));
465
496
 
466
- if (method === 'tools/list') {
467
- send({ jsonrpc: '2.0', id, result: { tools } }, true);
468
- return;
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
+ });
469
516
  }
470
517
 
471
- if (method === 'resources/list') {
472
- const resources = await listResources(subMgr.list());
473
- send({ jsonrpc: '2.0', id, result: { resources } }, true);
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();
474
524
  return;
475
525
  }
476
526
 
477
- if (method === 'resources/read') {
478
- const uri = params?.uri;
479
- if (!uri) {
480
- send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
481
- return;
482
- }
527
+ if (session) {
483
528
  try {
484
- const data = await readResource(uri);
485
- send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
529
+ await handleMcpRequest(session, method, params, id);
486
530
  } catch (e) {
487
- send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
531
+ log('SSE message error: ' + e.message);
532
+ session.sendError(-32603, e.message, id);
488
533
  }
489
- return;
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
+ );
490
543
  }
544
+ });
545
+ }
491
546
 
492
- if (method === 'resources/subscribe') {
493
- const uri = params?.uri;
494
- if (!uri) {
495
- send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
496
- return;
497
- }
498
- subMgr.subscribe(uri);
499
- send({ jsonrpc: '2.0', id, result: { success: true } }, true);
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' });
500
552
  return;
501
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
+ });
502
565
 
503
- if (method === 'resources/unsubscribe') {
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.unsubscribe(uri);
510
- send({ jsonrpc: '2.0', id, result: { success: true } }, true);
566
+ app.post('/mcp', mcpLimiter, async (req, res) => {
567
+ const body = req.body || {};
568
+ const { method, params, id } = body;
569
+ const accept = req.get('Accept') || '';
570
+ const wantsSse = accept.includes('text/event-stream');
571
+ const sessionId = req.get('mcp-session-id') || '';
572
+ log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
573
+
574
+ if (wantsSse) {
575
+ if (!enableSse) {
576
+ res.status(400).json({ error: 'SSE mode is disabled' });
511
577
  return;
512
578
  }
513
579
 
514
- if (method === 'tools/call') {
515
- const toolName = params?.name;
516
- if (!toolName) {
517
- send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
580
+ const clientId = sessionId || crypto.randomUUID();
581
+ res.setHeader('mcp-session-id', clientId);
582
+
583
+ let session;
584
+ if (sessionId && sseSessionManager.has(sessionId)) {
585
+ session = sseSessionManager.get(sessionId);
586
+ if (!id) {
587
+ res.status(204).end();
518
588
  return;
519
589
  }
520
- const result = await handleToolCall(toolName, params?.arguments || {});
521
- send({ jsonrpc: '2.0', id, result }, true);
590
+ await handleMcpRequest(session, method, params, id);
591
+ res.status(204).end();
522
592
  return;
523
593
  }
524
594
 
525
- send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
526
- } catch (e) {
527
- log('MCP error: ' + e.message);
528
- send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
595
+ session = new SseSession(res, { clientId });
596
+ sseSessionManager.add(clientId, session);
597
+
598
+ if (!id) {
599
+ return;
600
+ }
601
+ await handleMcpRequest(session, method, params, id);
602
+ return;
529
603
  }
604
+
605
+ res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
606
+ const { send } = createSseResponseHandler(res, wantsSse);
607
+
608
+ if (!id) return res.status(204).end();
609
+
610
+ await dispatchMcpMethod(
611
+ (response) => send(response, true),
612
+ method, params, id
613
+ );
530
614
  });
531
615
 
532
- app.listen(PORT, HOST, async () => {
616
+ const httpServer = app.listen(config.port, config.host, async () => {
533
617
  await initCadConnection();
534
- console.error(`@lisp MCP Server 启动 - http://${HOST}:${PORT} - Streamable HTTP`);
618
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP`);
535
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'));
536
631
  }
537
632
  }
538
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);