1- import { randomUUID } from 'node:crypto' ;
2-
1+ import { Server } from '@modelcontextprotocol/sdk/server/index.js' ;
32import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' ;
43
4+ import { config } from '@/config/manager' ;
5+ import { createStorage } from '@/core/storage/storageFactory' ;
6+ import { Storage } from '@/core/storage/types' ;
7+
58import { loggingContext } from '../http/context' ;
69
710export class TransportManager {
11+ private server : Server ;
12+ private storage : Storage ;
813 private transports : Map < string , StreamableHTTPServerTransport > = new Map ( ) ;
914
10- public getTransport (
15+ private readonly CACHE_KEY_PREFIX = 'mcp-session' ;
16+
17+ constructor ( server : Server ) {
18+ this . server = server ;
19+
20+ try {
21+ this . storage = createStorage ( config . storage ) ;
22+ } catch ( error : unknown ) {
23+ loggingContext . log ( 'error' , 'Failed to create storage' , {
24+ error : error instanceof Error ? error . message : 'Unknown error' ,
25+ stack : error instanceof Error ? error . stack : undefined ,
26+ } ) ;
27+ throw error ;
28+ }
29+ }
30+
31+ public async getTransport (
1132 sessionId : string
12- ) : StreamableHTTPServerTransport | undefined {
33+ ) : Promise < StreamableHTTPServerTransport | undefined > {
1334 loggingContext . log ( 'debug' , 'Getting transport' , {
1435 data : { sessionId } ,
1536 } ) ;
16- return this . transports . get ( sessionId ) ;
37+
38+ // If storage contains sessionId, then check transports with sessionId. If not, then create a new transport with sessionId.
39+ const session = await this . storage . get (
40+ `${ this . CACHE_KEY_PREFIX } :${ sessionId } `
41+ ) ;
42+ if ( session !== null && session . trim ( ) !== '' ) {
43+ if ( this . transports . has ( sessionId ) ) {
44+ loggingContext . log ( 'debug' , 'Transport found in transports' , {
45+ data : { sessionId } ,
46+ } ) ;
47+ return this . transports . get ( sessionId ) ;
48+ }
49+
50+ // It exists in storage, but not in transports. Create a new transport with sessionId.
51+ loggingContext . log (
52+ 'debug' ,
53+ 'Transport not found in transports, creating new transport'
54+ ) ;
55+ const newTransport = await this . createTransport ( sessionId ) ;
56+ this . transports . set ( sessionId , newTransport ) ;
57+
58+ return newTransport ;
59+ }
60+
61+ // If session is not found, then return undefined.
62+ return undefined ;
1763 }
1864
19- public hasTransport ( sessionId : string ) : boolean {
65+ public async hasTransport ( sessionId : string ) : Promise < boolean > {
2066 loggingContext . log ( 'debug' , 'Checking if transport exists' , {
2167 data : { sessionId } ,
2268 } ) ;
23- return this . transports . has ( sessionId ) ;
69+ const session = await this . storage . get (
70+ `${ this . CACHE_KEY_PREFIX } :${ sessionId } `
71+ ) ;
72+ return session !== null && session . trim ( ) !== '' ;
2473 }
2574
26- public createTransport ( ) : StreamableHTTPServerTransport {
27- const newSessionId = randomUUID ( ) ;
75+ public async createTransport (
76+ sessionId : string
77+ ) : Promise < StreamableHTTPServerTransport > {
2878 const transport = new StreamableHTTPServerTransport ( {
2979 /**
3080 * Function that generates a session ID for the transport.
3181 * The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash)
3282 *
3383 * Return undefined to disable session management.
3484 */
35- sessionIdGenerator : ( ) : string => newSessionId ,
85+ // This is disabled to make stateless mode.
86+ sessionIdGenerator : undefined ,
87+ // Below is for stateful mode.
88+ // sessionIdGenerator: (): string => sessionId,
3689 /**
3790 * If true, the server will return JSON responses instead of starting an SSE stream.
3891 * This can be useful for simple request/response scenarios without streaming.
@@ -42,45 +95,57 @@ export class TransportManager {
4295 } ) ;
4396
4497 // Manually set the session ID to ensure it's available
45- transport . sessionId = newSessionId ;
98+ transport . sessionId = sessionId ;
4699 loggingContext . log ( 'debug' , 'Creating transport' , {
47- data : { sessionId : newSessionId } ,
100+ data : { sessionId } ,
48101 } ) ;
49- this . transports . set ( newSessionId , transport ) ;
50102
51- loggingContext . log ( 'debug' , 'Transport created' , {
52- data : { sessionId : newSessionId } ,
53- } ) ;
103+ this . transports . set ( sessionId , transport ) ;
104+ await this . storage . set (
105+ `${ this . CACHE_KEY_PREFIX } :${ sessionId } ` ,
106+ JSON . stringify ( {
107+ createdAt : new Date ( ) . toISOString ( ) ,
108+ } )
109+ ) ;
110+
111+ loggingContext . log ( 'debug' , 'Transport created' ) ;
54112
55113 // Set up cleanup handler
56114 transport . onclose = ( ) : void => {
57115 const currentSessionId = transport . sessionId ;
58116 if ( currentSessionId !== undefined && currentSessionId . trim ( ) !== '' ) {
59117 this . transports . delete ( currentSessionId ) ;
118+ void this . storage . delete (
119+ `${ this . CACHE_KEY_PREFIX } :${ currentSessionId } `
120+ ) ;
60121 loggingContext . log ( 'debug' , 'Transport closed and cleaned up' , {
61122 data : {
62- sessionId : currentSessionId ,
63123 transportCount : this . transports . size ,
64124 } ,
65125 } ) ;
66126 }
67127 } ;
68128
129+ // Connect the transport to the server
130+ loggingContext . log ( 'debug' , 'Connecting transport to server' ) ;
131+ await this . server . connect (
132+ transport as StreamableHTTPServerTransport & { sessionId : string }
133+ ) ;
134+
69135 return transport ;
70136 }
71137
72- public deleteTransport ( sessionId : string ) : void {
138+ public async deleteTransport ( sessionId : string ) : Promise < void > {
73139 this . transports . delete ( sessionId ) ;
74- loggingContext . log ( 'info' , 'Transport deleted' , {
75- data : { sessionId } ,
76- } ) ;
140+ await this . storage . delete ( `${ this . CACHE_KEY_PREFIX } :${ sessionId } ` ) ;
141+ loggingContext . log ( 'info' , 'Transport deleted' ) ;
77142 }
78143
79144 public getTransportCount ( ) : number {
80145 return this . transports . size ;
81146 }
82147
83- public getAllSessionIds ( ) : string [ ] {
84- return Array . from ( this . transports . keys ( ) ) ;
148+ public async getAllSessionIds ( ) : Promise < string [ ] > {
149+ return this . storage . keys ( ` ${ this . CACHE_KEY_PREFIX } :*` ) ;
85150 }
86151}
0 commit comments