@atlisp/mcp 1.1.2 → 1.1.5

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
4
4
  "description": "MCP Server for @lisp on CAD",
5
5
  "type": "module",
6
6
  "bin": {
package/src/atlisp-mcp.js CHANGED
@@ -7,7 +7,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const require = createRequire(import.meta.url);
8
8
  const { version } = require(path.join(__dirname, '..', 'package.json'));
9
9
 
10
- if (process.argv[1]?.endsWith('atlisp-mcp.js')) {
10
+ const isMcpScript = process.argv[1]?.includes('atlisp-mcp');
11
+ if (isMcpScript) {
11
12
  const args = process.argv.slice(2);
12
13
  for (let i = 0; i < args.length; i++) {
13
14
  if (args[i] === '--version' || args[i] === '-v') {
@@ -34,6 +35,17 @@ Examples:
34
35
  atlisp-mcp --transport stdio # Same as --stdio
35
36
  `);
36
37
  process.exit(0);
38
+ } else if (args[i] === '--transport' && args[i + 1]) {
39
+ process.env.TRANSPORT = args[i + 1];
40
+ i++;
41
+ } else if (args[i] === '--stdio') {
42
+ process.env.TRANSPORT = 'stdio';
43
+ } else if (args[i] === '--port' && args[i + 1]) {
44
+ process.env.PORT = args[i + 1];
45
+ i++;
46
+ } else if (args[i] === '--host' && args[i + 1]) {
47
+ process.env.HOST = args[i + 1];
48
+ i++;
37
49
  }
38
50
  }
39
51
  }
@@ -43,11 +55,11 @@ import rawBody from 'raw-body';
43
55
  import iconv from 'iconv-lite';
44
56
  import rateLimit from 'express-rate-limit';
45
57
  import { randomUUID } from 'crypto';
58
+ import { AsyncLocalStorage } from 'async_hooks';
46
59
  const { decode: charsetDecode, encodingExists } = iconv;
47
60
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
48
61
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
49
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
50
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
62
+ import { SessionTransport } from './session-transport.js';
51
63
  import {
52
64
  ListToolsRequestSchema,
53
65
  CallToolRequestSchema,
@@ -67,9 +79,14 @@ import * as functionHandlers from './handlers/function-handlers.js';
67
79
  import { listResources, readResource } from './handlers/resource-handlers.js';
68
80
  import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
69
81
  import * as subMgr from './subscription-manager.js';
70
- 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';
71
85
  import config from './config.js';
72
86
 
87
+ const mcpContext = new AsyncLocalStorage();
88
+ const sseSessionManager = new SseSessionManager();
89
+
73
90
  const SERVER_CAPABILITIES = {
74
91
  tools: {},
75
92
  resources: { subscribe: true, listChanged: true },
@@ -125,6 +142,37 @@ const TOOL_HANDLERS = {
125
142
  }),
126
143
  };
127
144
 
145
+ function getOrCreateSessionServer(sessionId) {
146
+ let entry = sessionServers.get(sessionId);
147
+ if (!entry) {
148
+ const server = createMcpServer();
149
+ const transport = new SessionTransport(sessionId);
150
+ server.connect(transport);
151
+ entry = { server, transport };
152
+ sessionServers.set(sessionId, entry);
153
+ sessionTranports.set(sessionId, transport);
154
+ }
155
+ return entry;
156
+ }
157
+
158
+ function removeSessionServer(sessionId) {
159
+ const entry = sessionServers.get(sessionId);
160
+ if (entry) {
161
+ entry.transport.close().catch(() => {});
162
+ entry.server.close().catch(() => {});
163
+ sessionServers.delete(sessionId);
164
+ sessionTranports.delete(sessionId);
165
+ }
166
+ }
167
+
168
+ function broadcastToAllSessions(uri) {
169
+ for (const [sid, entry] of sessionServers) {
170
+ entry.server.sendResourceUpdated({ uri }).catch(err => {
171
+ log(`Failed to send resource update to session ${sid}: ${err.message}`);
172
+ });
173
+ }
174
+ }
175
+
128
176
  export const tools = [
129
177
  {
130
178
  name: 'connect_cad',
@@ -299,10 +347,11 @@ async function initCadConnection() {
299
347
  }
300
348
  }
301
349
 
302
- let server = null;
350
+ const sessionServers = new Map();
351
+ const sessionTranports = new Map();
303
352
 
304
353
  export function createMcpServer() {
305
- server = new Server(
354
+ const server = new Server(
306
355
  { name: SERVER_NAME, version: SERVER_VERSION },
307
356
  { capabilities: SERVER_CAPABILITIES }
308
357
  );
@@ -314,9 +363,14 @@ export function createMcpServer() {
314
363
  return await handleToolCall(name, args || {});
315
364
  });
316
365
 
317
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
318
- resources: await listResources(subMgr.list())
319
- }));
366
+ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
367
+ const ctx = mcpContext.getStore();
368
+ const sessionId = ctx?.sessionId;
369
+ const subscribedUris = sessionId ? subMgr.list(sessionId) : subMgr.list();
370
+ return {
371
+ resources: await listResources(subscribedUris)
372
+ };
373
+ });
320
374
 
321
375
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
322
376
  const { uri } = request.params;
@@ -340,13 +394,15 @@ export function createMcpServer() {
340
394
 
341
395
  server.setRequestHandler(SubscribeRequestSchema, async (request) => {
342
396
  const { uri } = request.params;
343
- subMgr.subscribe(uri);
397
+ const ctx = mcpContext.getStore();
398
+ subMgr.subscribe(uri, ctx?.sessionId);
344
399
  return { success: true };
345
400
  });
346
401
 
347
402
  server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
348
403
  const { uri } = request.params;
349
- subMgr.unsubscribe(uri);
404
+ const ctx = mcpContext.getStore();
405
+ subMgr.unsubscribe(uri, ctx?.sessionId);
350
406
  return { success: true };
351
407
  });
352
408
 
@@ -362,74 +418,18 @@ export function createMcpServer() {
362
418
  return server;
363
419
  }
364
420
 
365
- async function dispatchMcpMethod(responder, method, params, id) {
366
- try {
367
- if (method === 'initialize') {
368
- responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
369
- return;
370
- }
371
-
372
- const methodHandlers = {
373
- 'tools/list': async () => ({ tools }),
374
- 'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
375
- 'resources/read': async () => {
376
- const uri = params?.uri;
377
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
378
- const data = await readResource(uri);
379
- return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
380
- },
381
- 'resources/subscribe': async () => {
382
- const uri = params?.uri;
383
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
384
- subMgr.subscribe(uri);
385
- return { success: true };
386
- },
387
- 'resources/unsubscribe': async () => {
388
- const uri = params?.uri;
389
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
390
- subMgr.unsubscribe(uri);
391
- return { success: true };
392
- },
393
- 'tools/call': async () => {
394
- const toolName = params?.name;
395
- if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
396
- return await handleToolCall(toolName, params?.arguments || {});
397
- },
398
- 'prompts/list': async () => ({ prompts: await listPrompts() }),
399
- 'prompts/get': async () => {
400
- const promptName = params?.name;
401
- if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
402
- return await getPrompt(promptName, params?.arguments || {});
403
- }
404
- };
405
-
406
- const handler = methodHandlers[method];
407
- if (handler) {
408
- const result = await handler();
409
- responder({ jsonrpc: '2.0', id, result });
410
- } else {
411
- responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
412
- }
413
- } catch (e) {
414
- const mcpError = toMcpError(e);
415
- log(`MCP error [${method}]: ${mcpError.message}`);
416
- responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
417
- }
418
- }
419
421
 
420
- async function handleMcpRequest(session, method, params, id) {
421
- await dispatchMcpMethod(
422
- (response) => session.sendResponse(response),
423
- method, params, id
424
- );
425
- }
426
422
 
427
423
  export async function startServer() {
428
424
  if (config.transport === 'stdio') {
429
425
  await initCadConnection();
430
426
  const mcpServer = createMcpServer();
431
427
  const transport = new StdioServerTransport();
432
- await mcpServer.connect(transport);
428
+ const sessionId = 'stdio-session';
429
+ sessionManager.createSession(sessionId);
430
+ await mcpContext.run({ sessionId }, async () => {
431
+ await mcpServer.connect(transport);
432
+ });
433
433
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
434
434
 
435
435
  subMgr.setNotify((uri, data) => {
@@ -490,129 +490,115 @@ export async function startServer() {
490
490
  });
491
491
  }
492
492
 
493
- const mcpServer = createMcpServer();
494
-
495
- const transport = new StreamableHTTPServerTransport({
496
- sessionIdGenerator: () => randomUUID(),
497
- });
498
-
499
- await mcpServer.connect(transport);
500
- log('MCP Server connected to StreamableHTTPServerTransport');
501
-
502
493
  subMgr.setNotify((uri, data) => {
503
- mcpServer.sendResourceUpdated({ uri }).catch(err => {
504
- log(`Failed to send resource update: ${err.message}`);
505
- });
494
+ broadcastToAllSessions(uri);
495
+ for (const session of sseSessionManager.listActive()) {
496
+ sseSessionManager.sendToClient(session.clientId, 'resource_updated', { uri });
497
+ }
506
498
  });
507
499
 
508
- app.all('/mcp', mcpLimiter, async (req, res) => {
509
- try {
510
- await transport.handleRequest(req, res, req.body);
511
- } catch (error) {
512
- log(`Transport error: ${error.message}`);
513
- if (!res.headersSent) {
514
- res.status(500).json({ error: 'Internal server error' });
515
- }
500
+ async function handleMcpPost(req, res) {
501
+ const sessionId = req.headers['mcp-session-id'] || req.query.sessionId || randomUUID();
502
+ sessionManager.createSession(sessionId);
503
+ const body = req.body;
504
+ if (!body || !body.method) {
505
+ return res.status(400).json({ error: 'Invalid request' });
516
506
  }
517
- });
518
507
 
519
- app.all('/mcp/:path', mcpLimiter, async (req, res) => {
520
- try {
521
- await transport.handleRequest(req, res, req.body);
522
- } catch (error) {
523
- log(`Transport error: ${error.message}`);
524
- if (!res.headersSent) {
525
- res.status(500).json({ error: 'Internal server error' });
508
+ const { server, transport } = getOrCreateSessionServer(sessionId);
509
+ transport.resetCleanupTimer();
510
+
511
+ await mcpContext.run({ sessionId }, async () => {
512
+ try {
513
+ const response = await transport.handleRequest(body);
514
+ if (response) {
515
+ const headers = { 'Content-Type': 'application/json' };
516
+ if (transport.sessionId) {
517
+ headers['mcp-session-id'] = transport.sessionId;
518
+ }
519
+ res.writeHead(200, headers);
520
+ res.end(JSON.stringify(response));
521
+ } else {
522
+ if (!res.headersSent) {
523
+ res.status(202).end();
524
+ }
525
+ }
526
+ } catch (error) {
527
+ log(`Transport error [${sessionId}]: ${error.message}`);
528
+ if (!res.headersSent) {
529
+ res.status(500).json({ error: 'Internal server error', message: error.message });
530
+ }
526
531
  }
527
- }
528
- });
532
+ });
533
+ }
534
+
535
+ app.all('/mcp', mcpLimiter, handleMcpPost);
536
+ app.all('/mcp/:path', mcpLimiter, handleMcpPost);
537
+ app.get('/mcp/stream', mcpLimiter, handleMcpPost);
529
538
 
530
539
  const sseEndpoint = '/sse';
531
540
  app.get(sseEndpoint, async (req, res) => {
532
- const sseTransport = new SSEServerTransport(sseEndpoint, res);
533
- try {
534
- await mcpServer.connect(sseTransport);
535
- await sseTransport.handleRequest(req, res);
536
- } catch (error) {
537
- log(`SSE transport error: ${error.message}`);
538
- if (!res.headersSent) {
539
- res.status(500).json({ error: 'Internal server error' });
540
- }
541
- }
542
- });
543
- app.post(sseEndpoint, mcpLimiter, async (req, res) => {
544
- const sessionId = req.headers['mcp-session-id'];
545
- const sseTransport = sessionId ? sseEndpoint + '?sessionId=' + sessionId : sseEndpoint;
546
- try {
547
- const transport = new SSEServerTransport(sseEndpoint, res);
548
- await mcpServer.connect(transport);
549
- await transport.handleRequest(req, res);
550
- } catch (error) {
551
- log(`SSE POST error: ${error.message}`);
552
- if (!res.headersSent) {
553
- res.status(500).json({ error: 'Internal server error' });
554
- }
555
- }
556
- });
541
+ const sessionId = randomUUID();
542
+ sessionManager.createSession(sessionId);
543
+ getOrCreateSessionServer(sessionId);
557
544
 
558
- app.get('/mcp/sse', async (req, res) => {
559
- const sseTransport = new SSEServerTransport('/mcp/sse', res);
560
- try {
561
- await mcpServer.connect(sseTransport);
562
- await sseTransport.handleRequest(req, res);
563
- } catch (error) {
564
- log(`/mcp/sse transport error: ${error.message}`);
565
- if (!res.headersSent) {
566
- res.status(500).json({ error: 'Internal server error' });
567
- }
568
- }
569
- });
570
- app.post('/mcp/sse', mcpLimiter, 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 POST error: ${error.message}`);
577
- if (!res.headersSent) {
578
- res.status(500).json({ error: 'Internal server error' });
579
- }
580
- }
581
- });
545
+ const sseSession = new SseSession(res, { clientId: sessionId });
546
+ sseSessionManager.add(sessionId, sseSession);
582
547
 
583
- app.get('/mcp/stream', async (req, res) => {
584
- try {
585
- await transport.handleRequest(req, res, req.body);
586
- } catch (error) {
587
- log(`/mcp/stream error: ${error.message}`);
588
- if (!res.headersSent) {
589
- res.status(500).json({ error: 'Internal server error' });
590
- }
591
- }
548
+ sseSession.sendEndpoint(`/message?sessionId=${sessionId}`);
549
+
550
+ req.on('close', () => {
551
+ sseSessionManager.remove(sessionId);
552
+ removeSessionServer(sessionId);
553
+ });
592
554
  });
593
555
 
594
556
  app.post('/message', mcpLimiter, async (req, res) => {
595
- try {
596
- const sessionId = req.headers['mcp-session-id'];
597
- const sseTransport = new SSEServerTransport('/message', res);
598
- await mcpServer.connect(sseTransport);
599
- await sseTransport.handleRequest(req, res);
600
- } catch (error) {
601
- log(`/message error: ${error.message}`);
602
- if (!res.headersSent) {
603
- res.status(500).json({ error: 'Internal server error' });
557
+ const sessionId = req.query.sessionId;
558
+ if (!sessionId) {
559
+ return res.status(400).json({ error: 'Missing sessionId' });
560
+ }
561
+ const sseSession = sseSessionManager.get(sessionId);
562
+ if (!sseSession) {
563
+ return res.status(404).json({ error: 'Session not found' });
564
+ }
565
+ sessionManager.createSession(sessionId);
566
+ const { server, transport } = getOrCreateSessionServer(sessionId);
567
+ transport.resetCleanupTimer();
568
+
569
+ const body = req.body;
570
+ if (body && body.method) {
571
+ try {
572
+ const response = await transport.handleRequest(body);
573
+ if (response) {
574
+ sseSession.sendResponse(response);
575
+ }
576
+ if (!res.headersSent) {
577
+ res.status(202).end();
578
+ }
579
+ } catch (error) {
580
+ log(`SSE message error [${sessionId}]: ${error.message}`);
581
+ if (!res.headersSent) {
582
+ res.status(500).json({ error: 'Internal server error' });
583
+ }
604
584
  }
585
+ } else {
586
+ res.status(400).json({ error: 'Invalid request' });
605
587
  }
606
588
  });
607
589
 
608
590
  const httpServer = app.listen(config.port, config.host, async () => {
609
591
  await initCadConnection();
610
- console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP (SDK)`);
592
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} (多会话模式)`);
611
593
  });
612
594
 
613
595
  async function shutdown(signal) {
614
596
  console.error(`\n收到 ${signal},正在优雅关闭...`);
615
- await transport.close();
597
+ sseSessionManager.closeAll();
598
+ sessionManager.clear();
599
+ for (const [sid] of sessionServers) {
600
+ removeSessionServer(sid);
601
+ }
616
602
  await cad.disconnect();
617
603
  closeLog();
618
604
  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
 
package/src/config.js CHANGED
@@ -1,7 +1,29 @@
1
+ function parseArgs() {
2
+ const args = process.argv.slice(2);
3
+ const result = { transport: 'http', port: '8110', host: '0.0.0.0' };
4
+ for (let i = 0; i < args.length; i++) {
5
+ if (args[i] === '--transport' && args[i + 1]) {
6
+ result.transport = args[i + 1];
7
+ i++;
8
+ } else if (args[i] === '--stdio') {
9
+ result.transport = 'stdio';
10
+ } else if (args[i] === '--port' && args[i + 1]) {
11
+ result.port = args[i + 1];
12
+ i++;
13
+ } else if (args[i] === '--host' && args[i + 1]) {
14
+ result.host = args[i + 1];
15
+ i++;
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+
21
+ const cliArgs = parseArgs();
22
+
1
23
  const config = {
2
- port: parseInt(process.env.PORT || '8110', 10),
3
- host: process.env.HOST || '0.0.0.0',
4
- transport: process.env.TRANSPORT || 'http',
24
+ port: parseInt(process.env.PORT || cliArgs.port, 10),
25
+ host: process.env.HOST || cliArgs.host,
26
+ transport: process.env.TRANSPORT || cliArgs.transport,
5
27
  apiKey: process.env.MCP_API_KEY || '',
6
28
  enableSse: process.env.ENABLE_SSE !== 'false',
7
29
  messageTimeout: parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10),
@@ -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 };