Skip to content

Commit 8af07b5

Browse files
committed
sync: mcp-sap-docs 3e8fae30ec38acb5776397e2d23355983d8ce7d5 -> abap variant [skip-sync]
1 parent 711e5f5 commit 8af07b5

1 file changed

Lines changed: 27 additions & 9 deletions

File tree

src/streamable-http-server.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)