@@ -157,8 +157,14 @@ async function main() {
157157 method : req . method ,
158158 transportCount : Object . keys ( transports ) . length
159159 } ) ;
160- } else if ( ! sessionId && req . method === 'POST' && req . is ( 'application/json' ) && req . body ?. method === 'initialize' ) {
161- // New initialization request - create new transport
160+ } else if ( req . method === 'POST' && req . is ( 'application/json' ) && req . body ?. method === 'initialize' ) {
161+ // Initialization request — create a fresh transport.
162+ //
163+ // We also enter this branch if `sessionId` is present but doesn't match
164+ // any live transport (server restarted, session cleaned up via
165+ // `onsessionclosed`, in-memory map wiped). Per MCP spec the client is
166+ // permitted to re-send `initialize` to recover; the server generates a
167+ // new Mcp-Session-Id and the client should adopt it.
162168 const cleanupTransport = (
163169 sessionId : string | undefined ,
164170 trigger : "onsessionclosed" | "onclose" ,
@@ -211,12 +217,21 @@ async function main() {
211217 requestId,
212218 method : req . method
213219 } ) ;
214- } else if ( ! sessionId && req . method === 'POST' && req . is ( 'application/json' ) ) {
215- // Stateless request (no session ID, not initialize) — create one-shot transport
216- // This supports clients like Joule Studio that don't maintain sessions
217- logger . debug ( 'Stateless MCP request, creating one-shot transport' , {
220+ } else if ( req . method === 'POST' && req . is ( 'application/json' ) ) {
221+ // Stateless one-shot transport. Reached in two cases:
222+ // 1. No session ID (clients like Joule Studio that don't maintain sessions).
223+ // 2. Session ID present but unknown to the server (stale session — server
224+ // restarted, container redeployed, or session was cleaned up). Without
225+ // this fallback the client gets a hard HTTP 400 and many MCP clients
226+ // (notably Cursor) won't auto-recover by re-initializing, so the user
227+ // sees "No valid session ID" errors until they manually reconnect the
228+ // MCP. Treating stale sessions as one-shot trades a tiny amount of
229+ // per-session state for restart resilience — appropriate for a public,
230+ // read-only docs/search server.
231+ logger . debug ( 'Stateless / stale-session MCP request — creating one-shot transport' , {
218232 requestId,
219233 bodyMethod : req . body ?. method ,
234+ hasStaleSessionId : Boolean ( sessionId ) ,
220235 userAgent : req . headers [ 'user-agent' ]
221236 } ) ;
222237
@@ -227,12 +242,15 @@ async function main() {
227242 const server = createServer ( ) ;
228243 await server . connect ( transport ) ;
229244 } else {
230- // Invalid request - no session ID or not initialization request
245+ // Invalid request — only non-POST or non-JSON requests reach this branch
246+ // after the stateless-fallback above. Typical case: GET/DELETE /mcp
247+ // without a live session (the MCP spec uses POST for the actual JSON-RPC
248+ // traffic; GET/DELETE are only valid on an already-initialized stream).
231249 logger . warn ( 'Invalid MCP request' , {
232250 requestId,
233251 method : req . method ,
234252 hasSessionId : ! ! sessionId ,
235- isInitRequest : req . method === 'POST' && req . is ( 'application/json' ) && req . body ?. method === 'initialize ',
253+ contentType : req . headers [ 'content-type' ] || 'none ',
236254 sessionId : sessionId || 'none' ,
237255 userAgent : req . headers [ 'user-agent' ]
238256 } ) ;
@@ -241,7 +259,7 @@ async function main() {
241259 jsonrpc : '2.0' ,
242260 error : {
243261 code : - 32000 ,
244- message : 'Bad Request: No valid session ID provided or not an initialization request ' ,
262+ message : 'Bad Request: MCP requests must be POST with application/json ( or GET/DELETE on a live session). ' ,
245263 } ,
246264 id : null ,
247265 } ) ;
0 commit comments