@atlisp/mcp 1.1.3 → 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.3",
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
@@ -55,11 +55,11 @@ import rawBody from 'raw-body';
55
55
  import iconv from 'iconv-lite';
56
56
  import rateLimit from 'express-rate-limit';
57
57
  import { randomUUID } from 'crypto';
58
+ import { AsyncLocalStorage } from 'async_hooks';
58
59
  const { decode: charsetDecode, encodingExists } = iconv;
59
60
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
60
61
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
61
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
62
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
62
+ import { SessionTransport } from './session-transport.js';
63
63
  import {
64
64
  ListToolsRequestSchema,
65
65
  CallToolRequestSchema,
@@ -79,9 +79,14 @@ import * as functionHandlers from './handlers/function-handlers.js';
79
79
  import { listResources, readResource } from './handlers/resource-handlers.js';
80
80
  import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
81
81
  import * as subMgr from './subscription-manager.js';
82
- 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';
83
85
  import config from './config.js';
84
86
 
87
+ const mcpContext = new AsyncLocalStorage();
88
+ const sseSessionManager = new SseSessionManager();
89
+
85
90
  const SERVER_CAPABILITIES = {
86
91
  tools: {},
87
92
  resources: { subscribe: true, listChanged: true },
@@ -137,6 +142,37 @@ const TOOL_HANDLERS = {
137
142
  }),
138
143
  };
139
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
+
140
176
  export const tools = [
141
177
  {
142
178
  name: 'connect_cad',
@@ -311,10 +347,11 @@ async function initCadConnection() {
311
347
  }
312
348
  }
313
349
 
314
- let server = null;
350
+ const sessionServers = new Map();
351
+ const sessionTranports = new Map();
315
352
 
316
353
  export function createMcpServer() {
317
- server = new Server(
354
+ const server = new Server(
318
355
  { name: SERVER_NAME, version: SERVER_VERSION },
319
356
  { capabilities: SERVER_CAPABILITIES }
320
357
  );
@@ -326,9 +363,14 @@ export function createMcpServer() {
326
363
  return await handleToolCall(name, args || {});
327
364
  });
328
365
 
329
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
330
- resources: await listResources(subMgr.list())
331
- }));
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
+ });
332
374
 
333
375
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
334
376
  const { uri } = request.params;
@@ -352,13 +394,15 @@ export function createMcpServer() {
352
394
 
353
395
  server.setRequestHandler(SubscribeRequestSchema, async (request) => {
354
396
  const { uri } = request.params;
355
- subMgr.subscribe(uri);
397
+ const ctx = mcpContext.getStore();
398
+ subMgr.subscribe(uri, ctx?.sessionId);
356
399
  return { success: true };
357
400
  });
358
401
 
359
402
  server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
360
403
  const { uri } = request.params;
361
- subMgr.unsubscribe(uri);
404
+ const ctx = mcpContext.getStore();
405
+ subMgr.unsubscribe(uri, ctx?.sessionId);
362
406
  return { success: true };
363
407
  });
364
408
 
@@ -374,74 +418,18 @@ export function createMcpServer() {
374
418
  return server;
375
419
  }
376
420
 
377
- async function dispatchMcpMethod(responder, method, params, id) {
378
- try {
379
- if (method === 'initialize') {
380
- responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
381
- return;
382
- }
383
-
384
- const methodHandlers = {
385
- 'tools/list': async () => ({ tools }),
386
- 'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
387
- 'resources/read': async () => {
388
- const uri = params?.uri;
389
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
390
- const data = await readResource(uri);
391
- return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
392
- },
393
- 'resources/subscribe': async () => {
394
- const uri = params?.uri;
395
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
396
- subMgr.subscribe(uri);
397
- return { success: true };
398
- },
399
- 'resources/unsubscribe': async () => {
400
- const uri = params?.uri;
401
- if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
402
- subMgr.unsubscribe(uri);
403
- return { success: true };
404
- },
405
- 'tools/call': async () => {
406
- const toolName = params?.name;
407
- if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
408
- return await handleToolCall(toolName, params?.arguments || {});
409
- },
410
- 'prompts/list': async () => ({ prompts: await listPrompts() }),
411
- 'prompts/get': async () => {
412
- const promptName = params?.name;
413
- if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
414
- return await getPrompt(promptName, params?.arguments || {});
415
- }
416
- };
417
-
418
- const handler = methodHandlers[method];
419
- if (handler) {
420
- const result = await handler();
421
- responder({ jsonrpc: '2.0', id, result });
422
- } else {
423
- responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
424
- }
425
- } catch (e) {
426
- const mcpError = toMcpError(e);
427
- log(`MCP error [${method}]: ${mcpError.message}`);
428
- responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
429
- }
430
- }
431
421
 
432
- async function handleMcpRequest(session, method, params, id) {
433
- await dispatchMcpMethod(
434
- (response) => session.sendResponse(response),
435
- method, params, id
436
- );
437
- }
438
422
 
439
423
  export async function startServer() {
440
424
  if (config.transport === 'stdio') {
441
425
  await initCadConnection();
442
426
  const mcpServer = createMcpServer();
443
427
  const transport = new StdioServerTransport();
444
- 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
+ });
445
433
  console.error(`${SERVER_NAME} 运行在 stdio 模式`);
446
434
 
447
435
  subMgr.setNotify((uri, data) => {
@@ -502,129 +490,115 @@ export async function startServer() {
502
490
  });
503
491
  }
504
492
 
505
- const mcpServer = createMcpServer();
506
-
507
- const transport = new StreamableHTTPServerTransport({
508
- sessionIdGenerator: () => randomUUID(),
509
- });
510
-
511
- await mcpServer.connect(transport);
512
- log('MCP Server connected to StreamableHTTPServerTransport');
513
-
514
493
  subMgr.setNotify((uri, data) => {
515
- mcpServer.sendResourceUpdated({ uri }).catch(err => {
516
- log(`Failed to send resource update: ${err.message}`);
517
- });
494
+ broadcastToAllSessions(uri);
495
+ for (const session of sseSessionManager.listActive()) {
496
+ sseSessionManager.sendToClient(session.clientId, 'resource_updated', { uri });
497
+ }
518
498
  });
519
499
 
520
- app.all('/mcp', mcpLimiter, async (req, res) => {
521
- try {
522
- await transport.handleRequest(req, res, req.body);
523
- } catch (error) {
524
- log(`Transport error: ${error.message}`);
525
- if (!res.headersSent) {
526
- res.status(500).json({ error: 'Internal server error' });
527
- }
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' });
528
506
  }
529
- });
530
507
 
531
- app.all('/mcp/:path', mcpLimiter, async (req, res) => {
532
- try {
533
- await transport.handleRequest(req, res, req.body);
534
- } catch (error) {
535
- log(`Transport error: ${error.message}`);
536
- if (!res.headersSent) {
537
- 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
+ }
538
531
  }
539
- }
540
- });
532
+ });
533
+ }
534
+
535
+ app.all('/mcp', mcpLimiter, handleMcpPost);
536
+ app.all('/mcp/:path', mcpLimiter, handleMcpPost);
537
+ app.get('/mcp/stream', mcpLimiter, handleMcpPost);
541
538
 
542
539
  const sseEndpoint = '/sse';
543
540
  app.get(sseEndpoint, async (req, res) => {
544
- const sseTransport = new SSEServerTransport(sseEndpoint, res);
545
- try {
546
- await mcpServer.connect(sseTransport);
547
- await sseTransport.handleRequest(req, res);
548
- } catch (error) {
549
- log(`SSE transport error: ${error.message}`);
550
- if (!res.headersSent) {
551
- res.status(500).json({ error: 'Internal server error' });
552
- }
553
- }
554
- });
555
- app.post(sseEndpoint, mcpLimiter, async (req, res) => {
556
- const sessionId = req.headers['mcp-session-id'];
557
- const sseTransport = sessionId ? sseEndpoint + '?sessionId=' + sessionId : sseEndpoint;
558
- try {
559
- const transport = new SSEServerTransport(sseEndpoint, res);
560
- await mcpServer.connect(transport);
561
- await transport.handleRequest(req, res);
562
- } catch (error) {
563
- log(`SSE POST error: ${error.message}`);
564
- if (!res.headersSent) {
565
- res.status(500).json({ error: 'Internal server error' });
566
- }
567
- }
568
- });
541
+ const sessionId = randomUUID();
542
+ sessionManager.createSession(sessionId);
543
+ getOrCreateSessionServer(sessionId);
569
544
 
570
- app.get('/mcp/sse', 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 transport error: ${error.message}`);
577
- if (!res.headersSent) {
578
- res.status(500).json({ error: 'Internal server error' });
579
- }
580
- }
581
- });
582
- app.post('/mcp/sse', mcpLimiter, async (req, res) => {
583
- const sseTransport = new SSEServerTransport('/mcp/sse', res);
584
- try {
585
- await mcpServer.connect(sseTransport);
586
- await sseTransport.handleRequest(req, res);
587
- } catch (error) {
588
- log(`/mcp/sse POST error: ${error.message}`);
589
- if (!res.headersSent) {
590
- res.status(500).json({ error: 'Internal server error' });
591
- }
592
- }
593
- });
545
+ const sseSession = new SseSession(res, { clientId: sessionId });
546
+ sseSessionManager.add(sessionId, sseSession);
594
547
 
595
- app.get('/mcp/stream', async (req, res) => {
596
- try {
597
- await transport.handleRequest(req, res, req.body);
598
- } catch (error) {
599
- log(`/mcp/stream error: ${error.message}`);
600
- if (!res.headersSent) {
601
- res.status(500).json({ error: 'Internal server error' });
602
- }
603
- }
548
+ sseSession.sendEndpoint(`/message?sessionId=${sessionId}`);
549
+
550
+ req.on('close', () => {
551
+ sseSessionManager.remove(sessionId);
552
+ removeSessionServer(sessionId);
553
+ });
604
554
  });
605
555
 
606
556
  app.post('/message', mcpLimiter, async (req, res) => {
607
- try {
608
- const sessionId = req.headers['mcp-session-id'];
609
- const sseTransport = new SSEServerTransport('/message', res);
610
- await mcpServer.connect(sseTransport);
611
- await sseTransport.handleRequest(req, res);
612
- } catch (error) {
613
- log(`/message error: ${error.message}`);
614
- if (!res.headersSent) {
615
- 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
+ }
616
584
  }
585
+ } else {
586
+ res.status(400).json({ error: 'Invalid request' });
617
587
  }
618
588
  });
619
589
 
620
590
  const httpServer = app.listen(config.port, config.host, async () => {
621
591
  await initCadConnection();
622
- console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP (SDK)`);
592
+ console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} (多会话模式)`);
623
593
  });
624
594
 
625
595
  async function shutdown(signal) {
626
596
  console.error(`\n收到 ${signal},正在优雅关闭...`);
627
- await transport.close();
597
+ sseSessionManager.closeAll();
598
+ sessionManager.clear();
599
+ for (const [sid] of sessionServers) {
600
+ removeSessionServer(sid);
601
+ }
628
602
  await cad.disconnect();
629
603
  closeLog();
630
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
 
@@ -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 };