nitrostack 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/ARCHITECTURE.md +302 -0
  2. package/dist/cli/commands/build.js +1 -1
  3. package/dist/cli/commands/build.js.map +1 -1
  4. package/dist/cli/commands/start.js +3 -3
  5. package/dist/cli/commands/start.js.map +1 -1
  6. package/dist/cli/mcp-dev-wrapper.js +60 -12
  7. package/dist/cli/mcp-dev-wrapper.js.map +1 -1
  8. package/dist/core/events/log-emitter.d.ts +8 -0
  9. package/dist/core/events/log-emitter.d.ts.map +1 -0
  10. package/dist/core/events/log-emitter.js +20 -0
  11. package/dist/core/events/log-emitter.js.map +1 -0
  12. package/dist/core/logger.d.ts.map +1 -1
  13. package/dist/core/logger.js +3 -1
  14. package/dist/core/logger.js.map +1 -1
  15. package/dist/core/server.d.ts.map +1 -1
  16. package/dist/core/server.js +58 -14
  17. package/dist/core/server.js.map +1 -1
  18. package/dist/core/transports/streamable-http.d.ts +145 -0
  19. package/dist/core/transports/streamable-http.d.ts.map +1 -0
  20. package/dist/core/transports/streamable-http.js +691 -0
  21. package/dist/core/transports/streamable-http.js.map +1 -0
  22. package/package.json +2 -2
  23. package/src/studio/app/settings/page.tsx +8 -79
  24. package/src/studio/components/Sidebar.tsx +1 -1
  25. package/templates/typescript-auth/src/index.ts +27 -13
  26. package/templates/typescript-auth-api-key/src/index.ts +29 -14
  27. package/templates/typescript-auth-api-key/src/widgets/next.config.js +9 -1
  28. package/templates/typescript-oauth/src/index.ts +30 -9
  29. package/templates/typescript-oauth/src/widgets/next.config.js +9 -1
  30. package/templates/typescript-starter/src/index.ts +28 -4
  31. package/templates/typescript-starter/src/widgets/next.config.js +9 -1
  32. package/src/studio/package-lock.json +0 -3129
@@ -0,0 +1,691 @@
1
+ /**
2
+ * Streamable HTTP Transport for MCP
3
+ *
4
+ * Implements the MCP Streamable HTTP transport specification (2025-06-18).
5
+ * https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http
6
+ *
7
+ * Features:
8
+ * - Single MCP endpoint supporting both POST and GET
9
+ * - POST for sending messages to server
10
+ * - GET for SSE streams from server
11
+ * - Session management with Mcp-Session-Id header
12
+ * - Resumability support with Last-Event-ID
13
+ * - Multiple concurrent client connections
14
+ * - Protocol version header support
15
+ */
16
+ import express from 'express';
17
+ import { v4 as uuidv4 } from 'uuid';
18
+ /**
19
+ * Streamable HTTP Transport
20
+ * Implements MCP Streamable HTTP specification
21
+ */
22
+ export class StreamableHttpTransport {
23
+ app;
24
+ server = null;
25
+ sessions = new Map();
26
+ activeStreams = new Map(); // For sessionless mode
27
+ messageHandler;
28
+ closeHandler;
29
+ errorHandler;
30
+ options;
31
+ sessionCleanupInterval;
32
+ constructor(options = {}) {
33
+ this.options = {
34
+ port: options.port || 3000,
35
+ host: options.host || 'localhost',
36
+ endpoint: options.endpoint || '/mcp',
37
+ enableSessions: options.enableSessions === true, // Default to false for simpler clients
38
+ sessionTimeout: options.sessionTimeout || 30 * 60 * 1000, // 30 minutes
39
+ enableCors: options.enableCors !== false, // Default to true
40
+ };
41
+ this.app = options.app || express();
42
+ // CRITICAL: Disable Express's automatic OPTIONS handling
43
+ this.app.set('x-powered-by', false);
44
+ this.setupMiddleware();
45
+ this.setupRoutes();
46
+ this.startSessionCleanup();
47
+ }
48
+ /**
49
+ * Setup Express middleware
50
+ */
51
+ setupMiddleware() {
52
+ // CORS (if enabled) - MUST be the very first middleware, handles ALL requests
53
+ if (this.options.enableCors) {
54
+ // Add CORS headers to ALL responses
55
+ this.app.use((req, res, next) => {
56
+ res.setHeader('Access-Control-Allow-Origin', '*');
57
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
58
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID');
59
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
60
+ // Handle OPTIONS immediately
61
+ if (req.method === 'OPTIONS') {
62
+ res.status(200).end();
63
+ return;
64
+ }
65
+ next();
66
+ });
67
+ }
68
+ // Security: Validate Origin header to prevent DNS rebinding attacks (skip if CORS enabled)
69
+ if (!this.options.enableCors) {
70
+ this.app.use((req, res, next) => {
71
+ const origin = req.get('Origin');
72
+ const host = req.get('Host');
73
+ if (origin && host) {
74
+ const originHost = new URL(origin).host;
75
+ if (originHost !== host && !this.isLocalhost(originHost)) {
76
+ res.status(403).json({ error: 'Invalid Origin header' });
77
+ return;
78
+ }
79
+ }
80
+ next();
81
+ });
82
+ }
83
+ // JSON parsing
84
+ this.app.use(express.json());
85
+ }
86
+ /**
87
+ * Setup MCP endpoint routes
88
+ */
89
+ setupRoutes() {
90
+ const endpoint = this.options.endpoint;
91
+ // IMPORTANT: Add OPTIONS handlers FIRST to override Express's auto-OPTIONS
92
+ if (this.options.enableCors) {
93
+ // Main endpoint OPTIONS
94
+ this.app.options(endpoint, (req, res) => {
95
+ res.sendStatus(200);
96
+ });
97
+ // SSE endpoint OPTIONS
98
+ this.app.options(`${endpoint}/sse`, (req, res) => {
99
+ res.sendStatus(200);
100
+ });
101
+ // Message endpoint OPTIONS
102
+ this.app.options(`${endpoint}/message`, (req, res) => {
103
+ res.sendStatus(200);
104
+ });
105
+ }
106
+ // MCP Endpoint - POST for sending messages to server
107
+ this.app.post(endpoint, async (req, res) => {
108
+ await this.handlePost(req, res);
109
+ });
110
+ // MCP Endpoint - GET for SSE streams
111
+ this.app.get(endpoint, (req, res) => {
112
+ this.handleGet(req, res);
113
+ });
114
+ // MCP Endpoint - DELETE for session termination
115
+ this.app.delete(endpoint, (req, res) => {
116
+ this.handleDelete(req, res);
117
+ });
118
+ // Backward compatibility: /sse endpoint (alias for GET /mcp)
119
+ this.app.get(`${endpoint}/sse`, (req, res) => {
120
+ this.handleGet(req, res);
121
+ });
122
+ // Backward compatibility: /message endpoint (alias for POST /mcp)
123
+ this.app.post(`${endpoint}/message`, async (req, res) => {
124
+ // Simple message handler that doesn't require all the session/SSE logic
125
+ try {
126
+ const message = req.body;
127
+ if (!message || !message.jsonrpc) {
128
+ res.status(400).json({ error: 'Invalid JSON-RPC message' });
129
+ return;
130
+ }
131
+ // Pass to message handler
132
+ if (this.messageHandler) {
133
+ await this.messageHandler(message);
134
+ }
135
+ res.json({ status: 'received' });
136
+ }
137
+ catch (error) {
138
+ console.error('Error handling message:', error);
139
+ res.status(500).json({ error: error.message });
140
+ }
141
+ });
142
+ // Info endpoint for GET on /message
143
+ this.app.get(`${endpoint}/message`, (req, res) => {
144
+ res.json({
145
+ endpoint: `${endpoint}/message`,
146
+ method: 'POST',
147
+ description: 'Send JSON-RPC messages to the MCP server',
148
+ usage: 'POST with Content-Type: application/json',
149
+ example: {
150
+ jsonrpc: '2.0',
151
+ method: 'initialize',
152
+ params: {
153
+ protocolVersion: '2024-11-05',
154
+ capabilities: {},
155
+ clientInfo: { name: 'test-client', version: '1.0.0' }
156
+ },
157
+ id: 1
158
+ }
159
+ });
160
+ });
161
+ // Health check
162
+ this.app.get(`${endpoint}/health`, (req, res) => {
163
+ res.json({
164
+ status: 'ok',
165
+ transport: 'streamable-http',
166
+ version: '2025-06-18',
167
+ sessions: this.sessions.size,
168
+ uptime: process.uptime(),
169
+ });
170
+ });
171
+ }
172
+ /**
173
+ * Handle POST requests (client sending messages to server)
174
+ */
175
+ async handlePost(req, res) {
176
+ try {
177
+ const message = req.body;
178
+ const sessionId = req.get('Mcp-Session-Id');
179
+ const accept = req.get('Accept') || '';
180
+ // Validate JSON-RPC message
181
+ if (!message || !message.jsonrpc || message.jsonrpc !== '2.0') {
182
+ res.status(400).json({
183
+ jsonrpc: '2.0',
184
+ error: { code: -32600, message: 'Invalid JSON-RPC message' }
185
+ });
186
+ return;
187
+ }
188
+ // Check session
189
+ if (this.options.enableSessions && sessionId) {
190
+ const session = this.sessions.get(sessionId);
191
+ if (!session) {
192
+ res.status(404).json({
193
+ jsonrpc: '2.0',
194
+ error: { code: -32001, message: 'Session not found' }
195
+ });
196
+ return;
197
+ }
198
+ session.lastActivity = Date.now();
199
+ }
200
+ // Handle different message types
201
+ const messageType = this.getMessageType(message);
202
+ if (messageType === 'notification' || messageType === 'response') {
203
+ // Notification or Response: Return 202 Accepted
204
+ if (this.messageHandler) {
205
+ await this.messageHandler(message);
206
+ }
207
+ res.status(202).send();
208
+ return;
209
+ }
210
+ if (messageType === 'request') {
211
+ // Request: Accept header check (be lenient - if not specified, assume they want SSE)
212
+ const supportsSSE = !accept || accept.includes('text/event-stream') || accept.includes('*/*');
213
+ const supportsJSON = accept.includes('application/json');
214
+ // Pass to message handler
215
+ if (this.messageHandler) {
216
+ await this.messageHandler(message);
217
+ }
218
+ // For InitializeRequest, create session if enabled
219
+ if (this.isInitializeRequest(message)) {
220
+ if (this.options.enableSessions && !sessionId) {
221
+ const newSessionId = this.generateSessionId();
222
+ const session = {
223
+ id: newSessionId,
224
+ streams: new Map(),
225
+ lastActivity: Date.now(),
226
+ messageQueue: [],
227
+ eventIdCounter: 0,
228
+ };
229
+ this.sessions.set(newSessionId, session);
230
+ res.setHeader('Mcp-Session-Id', newSessionId);
231
+ }
232
+ }
233
+ // For SSE: Just acknowledge receipt, response will come via existing SSE stream
234
+ if (supportsSSE) {
235
+ // Accept the request
236
+ res.status(202).send();
237
+ // Response will be sent via the send() method to existing SSE streams
238
+ }
239
+ else {
240
+ // Single JSON response (less common)
241
+ res.setHeader('Content-Type', 'application/json');
242
+ // Response will be sent by the protocol layer
243
+ res._mcpWaitingForResponse = true;
244
+ res._mcpRequestId = message.id;
245
+ }
246
+ }
247
+ }
248
+ catch (error) {
249
+ console.error('POST error:', error);
250
+ res.status(500).json({
251
+ jsonrpc: '2.0',
252
+ error: { code: -32603, message: 'Internal error' }
253
+ });
254
+ }
255
+ }
256
+ /**
257
+ * Handle GET requests (client opening SSE stream)
258
+ */
259
+ handleGet(req, res) {
260
+ const sessionId = req.get('Mcp-Session-Id');
261
+ const lastEventId = req.get('Last-Event-ID');
262
+ const accept = req.get('Accept') || '';
263
+ // Check if client explicitly doesn't want SSE (e.g., asking for JSON only)
264
+ const rejectsSSE = accept && !accept.includes('*/*') && !accept.includes('text/event-stream') && accept.length > 0;
265
+ if (rejectsSSE) {
266
+ res.status(405).send('Method Not Allowed - This endpoint provides Server-Sent Events');
267
+ return;
268
+ }
269
+ // Check session
270
+ let session;
271
+ if (this.options.enableSessions) {
272
+ if (!sessionId) {
273
+ res.status(400).json({ error: 'Mcp-Session-Id required' });
274
+ return;
275
+ }
276
+ session = this.sessions.get(sessionId);
277
+ if (!session) {
278
+ res.status(404).json({ error: 'Session not found' });
279
+ return;
280
+ }
281
+ session.lastActivity = Date.now();
282
+ }
283
+ // Setup SSE
284
+ res.setHeader('Content-Type', 'text/event-stream');
285
+ res.setHeader('Cache-Control', 'no-cache');
286
+ res.setHeader('Connection', 'keep-alive');
287
+ res.flushHeaders();
288
+ // Create stream
289
+ const streamId = uuidv4();
290
+ const stream = {
291
+ id: streamId,
292
+ response: res,
293
+ eventIdCounter: 0,
294
+ closed: false,
295
+ };
296
+ // Send endpoint event immediately (required by MCP SDK)
297
+ // This tells the client where to POST messages
298
+ const endpointUrl = `${req.protocol}://${req.get('host')}${this.options.endpoint}`;
299
+ try {
300
+ res.write(`event: endpoint\n`);
301
+ res.write(`data: ${endpointUrl}\n\n`);
302
+ }
303
+ catch (error) {
304
+ console.error('Error sending endpoint event:', error);
305
+ stream.closed = true;
306
+ return;
307
+ }
308
+ // Add to session or activeStreams
309
+ if (session) {
310
+ session.streams.set(streamId, stream);
311
+ // Resume support: replay messages after lastEventId
312
+ if (lastEventId) {
313
+ this.replayMessages(session, stream, lastEventId);
314
+ }
315
+ }
316
+ else {
317
+ // Sessionless mode: track in activeStreams
318
+ this.activeStreams.set(streamId, stream);
319
+ }
320
+ // Handle client disconnect
321
+ req.on('close', () => {
322
+ stream.closed = true;
323
+ if (session) {
324
+ session.streams.delete(streamId);
325
+ }
326
+ else {
327
+ this.activeStreams.delete(streamId);
328
+ }
329
+ });
330
+ // Send ping every 30 seconds to keep connection alive
331
+ const pingInterval = setInterval(() => {
332
+ if (stream.closed) {
333
+ clearInterval(pingInterval);
334
+ return;
335
+ }
336
+ try {
337
+ res.write(': ping\n\n');
338
+ }
339
+ catch (error) {
340
+ clearInterval(pingInterval);
341
+ stream.closed = true;
342
+ }
343
+ }, 30000);
344
+ }
345
+ /**
346
+ * Handle DELETE requests (session termination)
347
+ */
348
+ handleDelete(req, res) {
349
+ const sessionId = req.get('Mcp-Session-Id');
350
+ if (!sessionId) {
351
+ res.status(400).json({ error: 'Mcp-Session-Id required' });
352
+ return;
353
+ }
354
+ const session = this.sessions.get(sessionId);
355
+ if (!session) {
356
+ res.status(404).json({ error: 'Session not found' });
357
+ return;
358
+ }
359
+ // Close all streams
360
+ for (const stream of session.streams.values()) {
361
+ try {
362
+ stream.response.end();
363
+ stream.closed = true;
364
+ }
365
+ catch (error) {
366
+ // Ignore
367
+ }
368
+ }
369
+ // Remove session
370
+ this.sessions.delete(sessionId);
371
+ res.status(200).json({ status: 'session terminated' });
372
+ }
373
+ /**
374
+ * Start SSE stream for a request
375
+ */
376
+ async startSSEStream(req, res, request, sessionId) {
377
+ // Setup SSE
378
+ res.setHeader('Content-Type', 'text/event-stream');
379
+ res.setHeader('Cache-Control', 'no-cache');
380
+ res.setHeader('Connection', 'keep-alive');
381
+ res.flushHeaders();
382
+ // Create stream
383
+ const streamId = uuidv4();
384
+ const stream = {
385
+ id: streamId,
386
+ response: res,
387
+ eventIdCounter: 0,
388
+ closed: false,
389
+ };
390
+ // Store stream reference for this request
391
+ req._mcpStreamId = streamId;
392
+ req._mcpStream = stream;
393
+ // Add to session or activeStreams
394
+ if (sessionId) {
395
+ const session = this.sessions.get(sessionId);
396
+ if (session) {
397
+ session.streams.set(streamId, stream);
398
+ }
399
+ }
400
+ else {
401
+ // Sessionless mode: track in activeStreams
402
+ this.activeStreams.set(streamId, stream);
403
+ }
404
+ // Handle client disconnect
405
+ req.on('close', () => {
406
+ stream.closed = true;
407
+ if (sessionId) {
408
+ const session = this.sessions.get(sessionId);
409
+ if (session) {
410
+ session.streams.delete(streamId);
411
+ }
412
+ }
413
+ else {
414
+ this.activeStreams.delete(streamId);
415
+ }
416
+ });
417
+ }
418
+ /**
419
+ * Send message to client(s)
420
+ */
421
+ async send(message) {
422
+ // Find target session/stream
423
+ // For responses, send to the stream that made the request
424
+ if (this.isResponse(message)) {
425
+ const response = message;
426
+ await this.sendToRequestStream(response);
427
+ return;
428
+ }
429
+ // For requests and notifications, send to all active streams
430
+ // First, send to session-based streams
431
+ for (const session of this.sessions.values()) {
432
+ for (const stream of session.streams.values()) {
433
+ if (!stream.closed) {
434
+ await this.sendToStream(stream, message, session);
435
+ }
436
+ }
437
+ }
438
+ // Then, send to sessionless streams
439
+ for (const stream of this.activeStreams.values()) {
440
+ if (!stream.closed) {
441
+ await this.sendToStreamSessionless(stream, message);
442
+ }
443
+ }
444
+ }
445
+ /**
446
+ * Send message to a specific stream
447
+ */
448
+ async sendToStream(stream, message, session) {
449
+ try {
450
+ const eventId = `${session.id}-${++stream.eventIdCounter}`;
451
+ const data = JSON.stringify(message);
452
+ stream.response.write(`id: ${eventId}\n`);
453
+ stream.response.write(`data: ${data}\n\n`);
454
+ // Store in queue for resumability
455
+ session.messageQueue.push({
456
+ message,
457
+ streamId: stream.id,
458
+ eventId,
459
+ });
460
+ }
461
+ catch (error) {
462
+ console.error('Error sending to stream:', error);
463
+ stream.closed = true;
464
+ }
465
+ }
466
+ /**
467
+ * Send response to the stream that made the request
468
+ */
469
+ async sendToRequestStream(response) {
470
+ // Find the stream associated with this request
471
+ // For session-based streams
472
+ for (const session of this.sessions.values()) {
473
+ for (const stream of session.streams.values()) {
474
+ if (!stream.closed) {
475
+ await this.sendToStream(stream, response, session);
476
+ // Close stream after sending response (per spec)
477
+ setTimeout(() => {
478
+ try {
479
+ stream.response.end();
480
+ stream.closed = true;
481
+ session.streams.delete(stream.id);
482
+ }
483
+ catch (error) {
484
+ // Ignore
485
+ }
486
+ }, 100);
487
+ }
488
+ }
489
+ }
490
+ // For sessionless streams
491
+ for (const stream of this.activeStreams.values()) {
492
+ if (!stream.closed) {
493
+ await this.sendToStreamSessionless(stream, response);
494
+ // Close stream after sending response (per spec)
495
+ setTimeout(() => {
496
+ try {
497
+ stream.response.end();
498
+ stream.closed = true;
499
+ this.activeStreams.delete(stream.id);
500
+ }
501
+ catch (error) {
502
+ // Ignore
503
+ }
504
+ }, 100);
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Send message to a sessionless stream
510
+ */
511
+ async sendToStreamSessionless(stream, message) {
512
+ try {
513
+ const eventId = `${stream.id}-${++stream.eventIdCounter}`;
514
+ const data = JSON.stringify(message);
515
+ stream.response.write(`id: ${eventId}\n`);
516
+ stream.response.write(`data: ${data}\n\n`);
517
+ }
518
+ catch (error) {
519
+ console.error('Error sending to sessionless stream:', error);
520
+ stream.closed = true;
521
+ this.activeStreams.delete(stream.id);
522
+ }
523
+ }
524
+ /**
525
+ * Replay messages for resumability
526
+ */
527
+ replayMessages(session, stream, lastEventId) {
528
+ const messages = session.messageQueue.filter((msg) => msg.streamId === stream.id && msg.eventId > lastEventId);
529
+ for (const { message, eventId } of messages) {
530
+ try {
531
+ const data = JSON.stringify(message);
532
+ stream.response.write(`id: ${eventId}\n`);
533
+ stream.response.write(`data: ${data}\n\n`);
534
+ }
535
+ catch (error) {
536
+ console.error('Error replaying message:', error);
537
+ break;
538
+ }
539
+ }
540
+ }
541
+ /**
542
+ * Start the HTTP server
543
+ */
544
+ async start() {
545
+ if (this.server) {
546
+ await this.close();
547
+ }
548
+ return new Promise((resolve, reject) => {
549
+ const errorHandler = (error) => {
550
+ console.error(`Failed to start Streamable HTTP transport: ${error.message}`);
551
+ this.server = null;
552
+ reject(error);
553
+ };
554
+ try {
555
+ const server = this.app.listen(this.options.port, this.options.host);
556
+ server.once('error', errorHandler);
557
+ server.once('listening', () => {
558
+ server.removeListener('error', errorHandler);
559
+ server.on('error', (error) => {
560
+ if (this.errorHandler) {
561
+ this.errorHandler(error);
562
+ }
563
+ });
564
+ this.server = server;
565
+ console.error(`🌐 MCP Streamable HTTP transport listening on http://${this.options.host}:${this.options.port}${this.options.endpoint}`);
566
+ console.error(` Protocol: MCP 2025-06-18`);
567
+ console.error(` Sessions: ${this.options.enableSessions ? 'enabled' : 'disabled'}`);
568
+ resolve();
569
+ });
570
+ }
571
+ catch (error) {
572
+ reject(error);
573
+ }
574
+ });
575
+ }
576
+ /**
577
+ * Close the transport
578
+ */
579
+ async close() {
580
+ // Clear session cleanup
581
+ if (this.sessionCleanupInterval) {
582
+ clearInterval(this.sessionCleanupInterval);
583
+ }
584
+ // Close all sessions
585
+ for (const session of this.sessions.values()) {
586
+ for (const stream of session.streams.values()) {
587
+ try {
588
+ stream.response.end();
589
+ stream.closed = true;
590
+ }
591
+ catch (error) {
592
+ // Ignore
593
+ }
594
+ }
595
+ }
596
+ this.sessions.clear();
597
+ // Close HTTP server
598
+ if (this.server) {
599
+ return new Promise((resolve) => {
600
+ const server = this.server;
601
+ this.server = null;
602
+ server.closeAllConnections?.();
603
+ server.close((err) => {
604
+ if (err) {
605
+ console.error('HTTP server close error:', err.message);
606
+ }
607
+ if (this.closeHandler) {
608
+ this.closeHandler();
609
+ }
610
+ resolve();
611
+ });
612
+ });
613
+ }
614
+ if (this.closeHandler) {
615
+ this.closeHandler();
616
+ }
617
+ }
618
+ /**
619
+ * Set message handler
620
+ */
621
+ set onmessage(handler) {
622
+ this.messageHandler = handler;
623
+ }
624
+ /**
625
+ * Set close handler
626
+ */
627
+ set onclose(handler) {
628
+ this.closeHandler = handler;
629
+ }
630
+ /**
631
+ * Set error handler
632
+ */
633
+ set onerror(handler) {
634
+ this.errorHandler = handler;
635
+ }
636
+ /**
637
+ * Start session cleanup interval
638
+ */
639
+ startSessionCleanup() {
640
+ this.sessionCleanupInterval = setInterval(() => {
641
+ const now = Date.now();
642
+ for (const [sessionId, session] of this.sessions.entries()) {
643
+ if (now - session.lastActivity > this.options.sessionTimeout) {
644
+ // Cleanup expired session
645
+ for (const stream of session.streams.values()) {
646
+ try {
647
+ stream.response.end();
648
+ stream.closed = true;
649
+ }
650
+ catch (error) {
651
+ // Ignore
652
+ }
653
+ }
654
+ this.sessions.delete(sessionId);
655
+ console.error(`Session ${sessionId} expired and cleaned up`);
656
+ }
657
+ }
658
+ }, 60000); // Check every minute
659
+ }
660
+ /**
661
+ * Helper methods
662
+ */
663
+ generateSessionId() {
664
+ return uuidv4();
665
+ }
666
+ getMessageType(message) {
667
+ if ('method' in message && 'id' in message)
668
+ return 'request';
669
+ if ('result' in message || 'error' in message)
670
+ return 'response';
671
+ return 'notification';
672
+ }
673
+ isResponse(message) {
674
+ return 'result' in message || 'error' in message;
675
+ }
676
+ isInitializeRequest(message) {
677
+ return 'method' in message && message.method === 'initialize';
678
+ }
679
+ isLocalhost(host) {
680
+ // Extract hostname without port (host can be "localhost:3000")
681
+ const hostname = host.split(':')[0];
682
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
683
+ }
684
+ /**
685
+ * Get the Express app (for adding custom routes)
686
+ */
687
+ getApp() {
688
+ return this.app;
689
+ }
690
+ }
691
+ //# sourceMappingURL=streamable-http.js.map