@atlisp/mcp 1.0.27 → 1.1.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.0.27",
3
+ "version": "1.1.0",
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,49 @@
1
+ import { fileURLToPath } from 'url';
2
+ import path from 'path';
3
+ import { SERVER_VERSION } from './constants.js';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ if (process.argv[1]?.endsWith('atlisp-mcp.js')) {
8
+ const args = process.argv.slice(2);
9
+ for (let i = 0; i < args.length; i++) {
10
+ if (args[i] === '--version' || args[i] === '-v') {
11
+ console.log(`atlisp-mcp v${SERVER_VERSION}`);
12
+ process.exit(0);
13
+ } else if (args[i] === '--help' || args[i] === '-h') {
14
+ console.log(`
15
+ @lisp MCP Server v${SERVER_VERSION}
16
+
17
+ Usage: atlisp-mcp [options]
18
+
19
+ Options:
20
+ -v, --version Show version number
21
+ --transport <type> Transport type: http or stdio (default: http)
22
+ --port <port> HTTP port (default: 8110)
23
+ --host <host> HTTP host (default: 0.0.0.0)
24
+ --stdio Shorthand for --transport stdio
25
+ -h, --help Show this help message
26
+
27
+ Examples:
28
+ atlisp-mcp # Start HTTP server on port 8110
29
+ atlisp-mcp --port 3000 # Start HTTP server on port 3000
30
+ atlisp-mcp --stdio # Start in stdio mode for MCP clients
31
+ atlisp-mcp --transport stdio # Same as --stdio
32
+ `);
33
+ process.exit(0);
34
+ }
35
+ }
36
+ }
37
+
1
38
  import express from 'express';
2
39
  import rawBody from 'raw-body';
3
40
  import iconv from 'iconv-lite';
4
41
  import rateLimit from 'express-rate-limit';
42
+ import { randomUUID } from 'crypto';
5
43
  const { decode: charsetDecode, encodingExists } = iconv;
6
44
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
45
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
46
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
47
  import {
9
48
  ListToolsRequestSchema,
10
49
  CallToolRequestSchema,
@@ -24,8 +63,6 @@ import * as functionHandlers from './handlers/function-handlers.js';
24
63
  import { listResources, readResource } from './handlers/resource-handlers.js';
25
64
  import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
26
65
  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
66
  import { createError, toMcpError } from './errors.js';
30
67
  import config from './config.js';
31
68
 
@@ -41,10 +78,9 @@ const SERVER_INFO = {
41
78
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
42
79
  };
43
80
 
44
- const { port, host, transport, apiKey, enableSse, rateLimitWindow, rateLimitMax } = config;
81
+ const { port, host, transport, apiKey, rateLimitWindow, rateLimitMax } = config;
45
82
 
46
83
  if (apiKey) log('API Key authentication enabled');
47
- if (enableSse) log('SSE mode enabled');
48
84
 
49
85
  const mcpLimiter = rateLimit({
50
86
  windowMs: rateLimitWindow,
@@ -54,16 +90,6 @@ const mcpLimiter = rateLimit({
54
90
  message: { error: '请求过于频繁,请稍后再试' }
55
91
  });
56
92
 
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
93
  export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
68
94
 
69
95
  const TOOL_HANDLERS = {
@@ -371,9 +397,15 @@ export async function startServer() {
371
397
  if (config.transport === 'stdio') {
372
398
  await initCadConnection();
373
399
  const mcpServer = createMcpServer();
374
- const transport = new StdioServerTransport();
400
+ const transport = new StdioServerTransport();
375
401
  await mcpServer.connect(transport);
376
402
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
403
+
404
+ subMgr.setNotify((uri, data) => {
405
+ mcpServer.sendResourceUpdated({ uri }).catch(err => {
406
+ log(`Failed to send resource update: ${err.message}`);
407
+ });
408
+ });
377
409
  } else {
378
410
  const app = express();
379
411
 
@@ -396,7 +428,7 @@ const transport = new StdioServerTransport();
396
428
  });
397
429
 
398
430
  app.use((req, res, next) => {
399
- log(`${req.method} ${req.path} - body: ${JSON.stringify(req.body)}`);
431
+ log(`${req.method} ${req.path}`);
400
432
  next();
401
433
  });
402
434
 
@@ -414,25 +446,6 @@ const transport = new StdioServerTransport();
414
446
  res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
415
447
  });
416
448
 
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
449
  if (config.enableCors) {
437
450
  app.use((req, res, next) => {
438
451
  res.setHeader('Access-Control-Allow-Origin', config.corsOrigin);
@@ -446,224 +459,58 @@ const transport = new StdioServerTransport();
446
459
  });
447
460
  }
448
461
 
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
- }
466
-
467
- if (lastEventId && lastEventId > 0) {
468
- session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
469
- }
470
-
471
- session.sendPing();
472
-
473
- res.on('close', () => {
474
- log(`SSE close: session ${clientId}`);
475
- sseSessionManager.remove(clientId);
476
- });
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
- }
462
+ const mcpServer = createMcpServer();
503
463
 
504
- const body = req.body || {};
505
- const { method, params, id } = body;
464
+ const transport = new StreamableHTTPServerTransport({
465
+ sessionIdGenerator: () => randomUUID(),
466
+ });
506
467
 
507
- if (!id) {
508
- if (session) res.status(204).end();
509
- else res.status(202).end();
510
- return;
511
- }
468
+ await mcpServer.connect(transport);
469
+ log('MCP Server connected to StreamableHTTPServerTransport');
512
470
 
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
- );
529
- }
471
+ subMgr.setNotify((uri, data) => {
472
+ mcpServer.sendResourceUpdated({ uri }).catch(err => {
473
+ log(`Failed to send resource update: ${err.message}`);
530
474
  });
531
- }
532
-
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;
545
- }
546
-
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;
560
- }
561
-
562
- session = new SseSession(res, { clientId });
563
- sseSessionManager.add(clientId, session);
564
-
565
- if (!id) {
566
- return;
567
- }
568
- await handleMcpRequest(session, method, params, id);
569
- return;
570
- }
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
475
  });
582
476
 
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;
477
+ app.all('/mcp', mcpLimiter, async (req, res) => {
478
+ try {
479
+ await transport.handleRequest(req, res, req.body);
480
+ } catch (error) {
481
+ log(`Transport error: ${error.message}`);
482
+ if (!res.headersSent) {
483
+ res.status(500).json({ error: 'Internal server error' });
589
484
  }
590
- setupSseConnection(req, res);
591
- return;
592
485
  }
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
486
  });
601
487
 
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;
614
- }
615
-
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;
629
- }
630
-
631
- session = new SseSession(res, { clientId });
632
- sseSessionManager.add(clientId, session);
633
-
634
- if (!id) {
635
- return;
488
+ app.all('/mcp/:path', mcpLimiter, async (req, res) => {
489
+ try {
490
+ await transport.handleRequest(req, res, req.body);
491
+ } catch (error) {
492
+ log(`Transport error: ${error.message}`);
493
+ if (!res.headersSent) {
494
+ res.status(500).json({ error: 'Internal server error' });
636
495
  }
637
- await handleMcpRequest(session, method, params, id);
638
- return;
639
496
  }
497
+ });
640
498
 
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
- );
499
+ const httpServer = app.listen(config.port, config.host, async () => {
500
+ await initCadConnection();
501
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP (SDK)`);
650
502
  });
651
503
 
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'));
504
+ async function shutdown(signal) {
505
+ console.error(`\n收到 ${signal},正在优雅关闭...`);
506
+ await transport.close();
507
+ await cad.disconnect();
508
+ closeLog();
509
+ httpServer.close(() => process.exit(0));
510
+ setTimeout(() => process.exit(1), 5000);
511
+ }
512
+ process.on('SIGINT', () => shutdown('SIGINT'));
513
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
667
514
  }
668
515
  }
669
516
 
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
- });