@atlisp/mcp 1.0.27 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.0.27",
3
+ "version": "1.1.1",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
7
- "atlisp-mcp": "./src/cli.js"
7
+ "atlisp-mcp": "./src/atlisp-mcp.js"
8
8
  },
9
9
  "main": "src/atlisp-mcp.js",
10
10
  "exports": {
package/src/atlisp-mcp.js CHANGED
@@ -1,10 +1,52 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { createRequire } from 'module';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const require = createRequire(import.meta.url);
7
+ const { version } = require(path.join(__dirname, '..', 'package.json'));
8
+
9
+ if (process.argv[1]?.endsWith('atlisp-mcp.js')) {
10
+ const args = process.argv.slice(2);
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === '--version' || args[i] === '-v') {
13
+ console.log(`atlisp-mcp v${version}`);
14
+ process.exit(0);
15
+ } else if (args[i] === '--help' || args[i] === '-h') {
16
+ console.log(`
17
+ @lisp MCP Server v${version}
18
+
19
+ Usage: atlisp-mcp [options]
20
+
21
+ Options:
22
+ -v, --version Show version number
23
+ --transport <type> Transport type: http or stdio (default: http)
24
+ --port <port> HTTP port (default: 8110)
25
+ --host <host> HTTP host (default: 0.0.0.0)
26
+ --stdio Shorthand for --transport stdio
27
+ -h, --help Show this help message
28
+
29
+ Examples:
30
+ atlisp-mcp # Start HTTP server on port 8110
31
+ atlisp-mcp --port 3000 # Start HTTP server on port 3000
32
+ atlisp-mcp --stdio # Start in stdio mode for MCP clients
33
+ atlisp-mcp --transport stdio # Same as --stdio
34
+ `);
35
+ process.exit(0);
36
+ }
37
+ }
38
+ }
39
+
1
40
  import express from 'express';
2
41
  import rawBody from 'raw-body';
3
42
  import iconv from 'iconv-lite';
4
43
  import rateLimit from 'express-rate-limit';
44
+ import { randomUUID } from 'crypto';
5
45
  const { decode: charsetDecode, encodingExists } = iconv;
6
46
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
47
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
48
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
49
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
8
50
  import {
9
51
  ListToolsRequestSchema,
10
52
  CallToolRequestSchema,
@@ -24,8 +66,6 @@ import * as functionHandlers from './handlers/function-handlers.js';
24
66
  import { listResources, readResource } from './handlers/resource-handlers.js';
25
67
  import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
26
68
  import * as subMgr from './subscription-manager.js';
27
- import { SseSession, SSE_EVENTS, createSseResponseHandler, parseLastEventId } from './sse-session.js';
28
- import { SseSessionManager } from './sse-session-manager.js';
29
69
  import { createError, toMcpError } from './errors.js';
30
70
  import config from './config.js';
31
71
 
@@ -41,10 +81,9 @@ const SERVER_INFO = {
41
81
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
42
82
  };
43
83
 
44
- const { port, host, transport, apiKey, enableSse, rateLimitWindow, rateLimitMax } = config;
84
+ const { port, host, transport, apiKey, rateLimitWindow, rateLimitMax } = config;
45
85
 
46
86
  if (apiKey) log('API Key authentication enabled');
47
- if (enableSse) log('SSE mode enabled');
48
87
 
49
88
  const mcpLimiter = rateLimit({
50
89
  windowMs: rateLimitWindow,
@@ -54,16 +93,6 @@ const mcpLimiter = rateLimit({
54
93
  message: { error: '请求过于频繁,请稍后再试' }
55
94
  });
56
95
 
57
- const sseSessionManager = new SseSessionManager();
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
-
67
96
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
68
97
 
69
98
  const TOOL_HANDLERS = {
@@ -371,9 +400,15 @@ export async function startServer() {
371
400
  if (config.transport === 'stdio') {
372
401
  await initCadConnection();
373
402
  const mcpServer = createMcpServer();
374
- const transport = new StdioServerTransport();
403
+ const transport = new StdioServerTransport();
375
404
  await mcpServer.connect(transport);
376
405
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
406
+
407
+ subMgr.setNotify((uri, data) => {
408
+ mcpServer.sendResourceUpdated({ uri }).catch(err => {
409
+ log(`Failed to send resource update: ${err.message}`);
410
+ });
411
+ });
377
412
  } else {
378
413
  const app = express();
379
414
 
@@ -396,7 +431,7 @@ const transport = new StdioServerTransport();
396
431
  });
397
432
 
398
433
  app.use((req, res, next) => {
399
- log(`${req.method} ${req.path} - body: ${JSON.stringify(req.body)}`);
434
+ log(`${req.method} ${req.path}`);
400
435
  next();
401
436
  });
402
437
 
@@ -414,25 +449,6 @@ const transport = new StdioServerTransport();
414
449
  res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
415
450
  });
416
451
 
417
- app.use((req, res, next) => {
418
- const path = req.path;
419
- if (path.startsWith('/%7B') || path.startsWith('/{"')) {
420
- try {
421
- const decoded = decodeURIComponent(path.slice(1));
422
- if (decoded.startsWith('{"path":')) {
423
- const data = JSON.parse(decoded);
424
- const endpoint = data.path;
425
- log(`Client sent SSE endpoint in URL path, extracting: ${endpoint}`);
426
- req.url = endpoint + ((req._parsedUrl && req._parsedUrl.search) || '');
427
- try { req.path = endpoint; } catch (_) { /* Express 5: req.path is read-only, req.url is sufficient */ }
428
- }
429
- } catch (e) {
430
- log(`Failed to parse malformed path: ${e.message}`);
431
- }
432
- }
433
- next();
434
- });
435
-
436
452
  if (config.enableCors) {
437
453
  app.use((req, res, next) => {
438
454
  res.setHeader('Access-Control-Allow-Origin', config.corsOrigin);
@@ -446,224 +462,136 @@ const transport = new StdioServerTransport();
446
462
  });
447
463
  }
448
464
 
449
- createMcpServer();
450
-
451
- function setupSseConnection(req, res) {
452
- log(`SSE connect: ${req.method} ${req.path} - session: ${req.get('mcp-session-id') || 'new'}`);
453
- const sessionId = req.get('mcp-session-id');
454
- const lastEventId = parseLastEventId(req);
455
- const clientId = sessionId || crypto.randomUUID();
456
-
457
- res.setHeader('mcp-session-id', clientId);
458
-
459
- let session;
460
- if (sessionId && sseSessionManager.has(sessionId)) {
461
- session = sseSessionManager.get(sessionId);
462
- } else {
463
- session = new SseSession(res, { clientId, resumeFromId: lastEventId });
464
- sseSessionManager.add(clientId, session);
465
- }
465
+ const mcpServer = createMcpServer();
466
466
 
467
- if (lastEventId && lastEventId > 0) {
468
- session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
469
- }
467
+ const transport = new StreamableHTTPServerTransport({
468
+ sessionIdGenerator: () => randomUUID(),
469
+ });
470
470
 
471
- session.sendPing();
471
+ await mcpServer.connect(transport);
472
+ log('MCP Server connected to StreamableHTTPServerTransport');
472
473
 
473
- res.on('close', () => {
474
- log(`SSE close: session ${clientId}`);
475
- sseSessionManager.remove(clientId);
474
+ subMgr.setNotify((uri, data) => {
475
+ mcpServer.sendResourceUpdated({ uri }).catch(err => {
476
+ log(`Failed to send resource update: ${err.message}`);
476
477
  });
477
- }
478
-
479
- if (enableSse) {
480
- app.get('/sse', (req, res) => setupSseConnection(req, res));
481
- app.get('/mcp/sse', (req, res) => setupSseConnection(req, res));
482
-
483
- app.post('/mcp/message', mcpLimiter, async (req, res) => {
484
- const sessionId = req.get('mcp-session-id') || '';
485
- const accept = req.get('Accept') || '';
486
- const wantsSse = accept.includes('text/event-stream');
487
- log(`POST /mcp/message - session: ${sessionId || 'new'} - wantsSSE: ${wantsSse}`);
488
-
489
- let session;
490
- if (sessionId && sseSessionManager.has(sessionId)) {
491
- session = sseSessionManager.get(sessionId);
492
- } else if (wantsSse) {
493
- const clientId = sessionId || crypto.randomUUID();
494
- res.setHeader('mcp-session-id', clientId);
495
- session = new SseSession(res, { clientId });
496
- sseSessionManager.add(clientId, session);
497
- log(`POST /mcp/message - created new session: ${clientId}`);
498
- res.on('close', () => {
499
- log(`POST SSE close: session ${clientId}`);
500
- sseSessionManager.remove(clientId);
501
- });
502
- }
503
-
504
- const body = req.body || {};
505
- const { method, params, id } = body;
506
-
507
- if (!id) {
508
- if (session) res.status(204).end();
509
- else res.status(202).end();
510
- return;
511
- }
478
+ });
512
479
 
513
- if (session) {
514
- try {
515
- await handleMcpRequest(session, method, params, id);
516
- } catch (e) {
517
- log('SSE message error: ' + e.message);
518
- session.sendError(-32603, e.message, id);
519
- }
520
- res.status(204).end();
521
- } else {
522
- await dispatchMcpMethod(
523
- (response) => {
524
- res.setHeader('Content-Type', 'application/json');
525
- res.status(200).json(response);
526
- },
527
- method, params, id
528
- );
480
+ app.all('/mcp', mcpLimiter, async (req, res) => {
481
+ try {
482
+ await transport.handleRequest(req, res, req.body);
483
+ } catch (error) {
484
+ log(`Transport error: ${error.message}`);
485
+ if (!res.headersSent) {
486
+ res.status(500).json({ error: 'Internal server error' });
529
487
  }
530
- });
531
- }
488
+ }
489
+ });
532
490
 
533
- app.post('/mcp/stream', mcpLimiter, async (req, res) => {
534
- const body = req.body || {};
535
- const { method, params, id } = body;
536
- const accept = req.get('Accept') || '';
537
- const wantsSse = accept.includes('text/event-stream');
538
- const sessionId = req.get('mcp-session-id') || '';
539
- log(`POST /mcp/stream - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
540
-
541
- if (wantsSse) {
542
- if (!enableSse) {
543
- res.status(400).json({ error: 'SSE mode is disabled' });
544
- return;
491
+ app.all('/mcp/:path', mcpLimiter, async (req, res) => {
492
+ try {
493
+ await transport.handleRequest(req, res, req.body);
494
+ } catch (error) {
495
+ log(`Transport error: ${error.message}`);
496
+ if (!res.headersSent) {
497
+ res.status(500).json({ error: 'Internal server error' });
545
498
  }
499
+ }
500
+ });
546
501
 
547
- const clientId = sessionId || crypto.randomUUID();
548
- res.setHeader('mcp-session-id', clientId);
549
-
550
- let session;
551
- if (sessionId && sseSessionManager.has(sessionId)) {
552
- session = sseSessionManager.get(sessionId);
553
- if (!id) {
554
- res.status(204).end();
555
- return;
556
- }
557
- await handleMcpRequest(session, method, params, id);
558
- res.status(204).end();
559
- return;
502
+ const sseEndpoint = '/sse';
503
+ app.get(sseEndpoint, async (req, res) => {
504
+ const sseTransport = new SSEServerTransport(sseEndpoint, res);
505
+ try {
506
+ await mcpServer.connect(sseTransport);
507
+ await sseTransport.handleRequest(req, res);
508
+ } catch (error) {
509
+ log(`SSE transport error: ${error.message}`);
510
+ if (!res.headersSent) {
511
+ res.status(500).json({ error: 'Internal server error' });
560
512
  }
561
-
562
- session = new SseSession(res, { clientId });
563
- sseSessionManager.add(clientId, session);
564
-
565
- if (!id) {
566
- return;
513
+ }
514
+ });
515
+ app.post(sseEndpoint, mcpLimiter, async (req, res) => {
516
+ const sessionId = req.headers['mcp-session-id'];
517
+ const sseTransport = sessionId ? sseEndpoint + '?sessionId=' + sessionId : sseEndpoint;
518
+ try {
519
+ const transport = new SSEServerTransport(sseEndpoint, res);
520
+ await mcpServer.connect(transport);
521
+ await transport.handleRequest(req, res);
522
+ } catch (error) {
523
+ log(`SSE POST error: ${error.message}`);
524
+ if (!res.headersSent) {
525
+ res.status(500).json({ error: 'Internal server error' });
567
526
  }
568
- await handleMcpRequest(session, method, params, id);
569
- return;
570
527
  }
571
-
572
- res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
573
- const { send } = createSseResponseHandler(res, wantsSse);
574
-
575
- if (!id) return res.status(204).end();
576
-
577
- await dispatchMcpMethod(
578
- (response) => send(response, true),
579
- method, params, id
580
- );
581
528
  });
582
529
 
583
- app.get('/mcp', (req, res) => {
584
- const accept = req.get('Accept') || '';
585
- if (accept.includes('text/event-stream')) {
586
- if (!enableSse) {
587
- res.status(400).json({ error: 'SSE mode is disabled' });
588
- return;
530
+ app.get('/mcp/sse', async (req, res) => {
531
+ const sseTransport = new SSEServerTransport('/mcp/sse', res);
532
+ try {
533
+ await mcpServer.connect(sseTransport);
534
+ await sseTransport.handleRequest(req, res);
535
+ } catch (error) {
536
+ log(`/mcp/sse transport error: ${error.message}`);
537
+ if (!res.headersSent) {
538
+ res.status(500).json({ error: 'Internal server error' });
589
539
  }
590
- setupSseConnection(req, res);
591
- return;
592
540
  }
593
- res.status(200).json({
594
- name: SERVER_NAME,
595
- version: SERVER_VERSION,
596
- transport: 'streamable-http',
597
- sseEndpoint: '/sse',
598
- messageEndpoint: '/mcp/message'
599
- });
600
541
  });
601
-
602
- app.post('/mcp', mcpLimiter, async (req, res) => {
603
- const body = req.body || {};
604
- const { method, params, id } = body;
605
- const accept = req.get('Accept') || '';
606
- const wantsSse = accept.includes('text/event-stream');
607
- const sessionId = req.get('mcp-session-id') || '';
608
- log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
609
-
610
- if (wantsSse) {
611
- if (!enableSse) {
612
- res.status(400).json({ error: 'SSE mode is disabled' });
613
- return;
542
+ app.post('/mcp/sse', mcpLimiter, async (req, res) => {
543
+ const sseTransport = new SSEServerTransport('/mcp/sse', res);
544
+ try {
545
+ await mcpServer.connect(sseTransport);
546
+ await sseTransport.handleRequest(req, res);
547
+ } catch (error) {
548
+ log(`/mcp/sse POST error: ${error.message}`);
549
+ if (!res.headersSent) {
550
+ res.status(500).json({ error: 'Internal server error' });
614
551
  }
552
+ }
553
+ });
615
554
 
616
- const clientId = sessionId || crypto.randomUUID();
617
- res.setHeader('mcp-session-id', clientId);
618
-
619
- let session;
620
- if (sessionId && sseSessionManager.has(sessionId)) {
621
- session = sseSessionManager.get(sessionId);
622
- if (!id) {
623
- res.status(204).end();
624
- return;
625
- }
626
- await handleMcpRequest(session, method, params, id);
627
- res.status(204).end();
628
- return;
555
+ app.get('/mcp/stream', async (req, res) => {
556
+ try {
557
+ await transport.handleRequest(req, res, req.body);
558
+ } catch (error) {
559
+ log(`/mcp/stream error: ${error.message}`);
560
+ if (!res.headersSent) {
561
+ res.status(500).json({ error: 'Internal server error' });
629
562
  }
563
+ }
564
+ });
630
565
 
631
- session = new SseSession(res, { clientId });
632
- sseSessionManager.add(clientId, session);
633
-
634
- if (!id) {
635
- return;
566
+ app.post('/message', mcpLimiter, async (req, res) => {
567
+ try {
568
+ const sessionId = req.headers['mcp-session-id'];
569
+ const sseTransport = new SSEServerTransport('/message', res);
570
+ await mcpServer.connect(sseTransport);
571
+ await sseTransport.handleRequest(req, res);
572
+ } catch (error) {
573
+ log(`/message error: ${error.message}`);
574
+ if (!res.headersSent) {
575
+ res.status(500).json({ error: 'Internal server error' });
636
576
  }
637
- await handleMcpRequest(session, method, params, id);
638
- return;
639
577
  }
578
+ });
640
579
 
641
- res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
642
- const { send } = createSseResponseHandler(res, wantsSse);
643
-
644
- if (!id) return res.status(204).end();
645
-
646
- await dispatchMcpMethod(
647
- (response) => send(response, true),
648
- method, params, id
649
- );
580
+ const httpServer = app.listen(config.port, config.host, async () => {
581
+ await initCadConnection();
582
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP (SDK)`);
650
583
  });
651
584
 
652
- const httpServer = app.listen(config.port, config.host, async () => {
653
- await initCadConnection();
654
- console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP`);
655
- });
656
-
657
- async function shutdown(signal) {
658
- console.error(`\n收到 ${signal},正在优雅关闭...`);
659
- sseSessionManager.closeAll();
660
- await cad.disconnect();
661
- closeLog();
662
- httpServer.close(() => process.exit(0));
663
- setTimeout(() => process.exit(1), 5000);
664
- }
665
- process.on('SIGINT', () => shutdown('SIGINT'));
666
- process.on('SIGTERM', () => shutdown('SIGTERM'));
585
+ async function shutdown(signal) {
586
+ console.error(`\n收到 ${signal},正在优雅关闭...`);
587
+ await transport.close();
588
+ await cad.disconnect();
589
+ closeLog();
590
+ httpServer.close(() => process.exit(0));
591
+ setTimeout(() => process.exit(1), 5000);
592
+ }
593
+ process.on('SIGINT', () => shutdown('SIGINT'));
594
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
667
595
  }
668
596
  }
669
597
 
package/src/cli.js DELETED
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawn } from 'child_process';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { SERVER_VERSION } from './constants.js';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const serverPath = path.join(__dirname, 'atlisp-mcp.js');
9
-
10
- const args = process.argv.slice(2);
11
- const env = { ...process.env };
12
-
13
- for (let i = 0; i < args.length; i++) {
14
- if (args[i] === '--version' || args[i] === '-v') {
15
- console.log(`atlisp-mcp v${SERVER_VERSION}`);
16
- process.exit(0);
17
- } else if (args[i] === '--transport' && args[i + 1]) {
18
- env.TRANSPORT = args[i + 1];
19
- i++;
20
- } else if (args[i] === '--port' && args[i + 1]) {
21
- env.PORT = args[i + 1];
22
- i++;
23
- } else if (args[i] === '--host' && args[i + 1]) {
24
- env.HOST = args[i + 1];
25
- i++;
26
- } else if (args[i] === '--stdio') {
27
- env.TRANSPORT = 'stdio';
28
- } else if (args[i] === '--help' || args[i] === '-h') {
29
- console.log(`
30
- @lisp MCP Server v${SERVER_VERSION}
31
-
32
- Usage: atlisp-mcp [options]
33
-
34
- Options:
35
- -v, --version Show version number
36
- --transport <type> Transport type: http or stdio (default: http)
37
- --port <port> HTTP port (default: 8110)
38
- --host <host> HTTP host (default: 0.0.0.0)
39
- --stdio Shorthand for --transport stdio
40
- -h, --help Show this help message
41
-
42
- Examples:
43
- atlisp-mcp # Start HTTP server on port 8110
44
- atlisp-mcp --port 3000 # Start HTTP server on port 3000
45
- atlisp-mcp --stdio # Start in stdio mode for MCP clients
46
- atlisp-mcp --transport stdio # Same as --stdio
47
- `);
48
- process.exit(0);
49
- }
50
- }
51
-
52
- const child = spawn('node', [serverPath], {
53
- env,
54
- stdio: 'inherit'
55
- });
56
-
57
- child.on('exit', (code) => {
58
- process.exit(code);
59
- });