From 06a672dd4a8e704b484df4436cca7e9ee1f5bdac Mon Sep 17 00:00:00 2001 From: Joe Wanko Date: Wed, 18 Mar 2026 23:59:36 -0400 Subject: [PATCH 1/2] fix(examples): return 404 for invalid session IDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #389 The streamable HTTP server examples were returning 400 for both missing and invalid session IDs. Per the MCP spec: - Missing session ID (non-init request) → 400 Bad Request - Invalid session ID (not found) → 404 Not Found The 404 status is important because it signals to clients that they should start a new session (per spec: 'When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session'). Updated the following examples: - jsonResponseStreamableHttp.ts - simpleStreamableHttp.ts (POST, GET, DELETE handlers) - standaloneSseWithGetStreamableHttp.ts (POST, GET handlers) - elicitationFormExample.ts (POST, GET, DELETE handlers) - elicitationUrlExample.ts (POST, GET, DELETE handlers) - simpleTaskInteractive.ts (POST, GET, DELETE handlers) --- examples/server/src/elicitationFormExample.ts | 39 ++++++++++++++++--- examples/server/src/elicitationUrlExample.ts | 39 ++++++++++++++++--- .../server/src/jsonResponseStreamableHttp.ts | 19 ++++++++- examples/server/src/simpleStreamableHttp.ts | 39 ++++++++++++++++--- examples/server/src/simpleTaskInteractive.ts | 35 ++++++++++++++--- .../src/standaloneSseWithGetStreamableHttp.ts | 29 ++++++++++++-- 6 files changed, 171 insertions(+), 29 deletions(-) diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index 9c13b739e..e7feb78d3 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -338,6 +338,19 @@ async function main() { if (sessionId && transports[sessionId]) { // Reuse existing transport for this session transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: 'Session not found' + }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - create new transport transport = new NodeStreamableHTTPServerTransport({ @@ -365,12 +378,14 @@ async function main() { await transport.handleRequest(req, res, req.body); return; } else { - // Invalid request - no session ID or not initialization request + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, - message: 'Bad Request: No valid session ID provided' + message: 'Bad Request: Session ID required' }, id: null }); @@ -399,8 +414,14 @@ async function main() { // Handle GET requests for SSE streams const mcpGetHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } @@ -414,8 +435,14 @@ async function main() { // Handle DELETE requests for session termination const mcpDeleteHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index c38dd75e8..272f42b0a 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -571,6 +571,19 @@ const mcpPostHandler = async (req: Request, res: Response) => { if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: 'Session not found' + }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { const server = getServer(); // New initialization request @@ -607,12 +620,14 @@ const mcpPostHandler = async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled } else { - // Invalid request - no session ID or not initialization request + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, - message: 'Bad Request: No valid session ID provided' + message: 'Bad Request: Session ID required' }, id: null }); @@ -643,8 +658,14 @@ app.post('/mcp', authMiddleware, mcpPostHandler); // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) const mcpGetHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } @@ -682,8 +703,14 @@ app.get('/mcp', authMiddleware, mcpGetHandler); // Handle DELETE requests for session termination (according to MCP spec) const mcpDeleteHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 7a3aad67a..3e40495d1 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -92,6 +92,19 @@ app.post('/mcp', async (req: Request, res: Response) => { if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: 'Session not found' + }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - use JSON response mode transport = new NodeStreamableHTTPServerTransport({ @@ -111,12 +124,14 @@ app.post('/mcp', async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled } else { - // Invalid request - no session ID or not initialization request + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, - message: 'Bad Request: No valid session ID provided' + message: 'Bad Request: Session ID required' }, id: null }); diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index be025c04c..1833a0760 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -654,6 +654,19 @@ const mcpPostHandler = async (req: Request, res: Response) => { if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: 'Session not found' + }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request const eventStore = new InMemoryEventStore(); @@ -685,12 +698,14 @@ const mcpPostHandler = async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled } else { - // Invalid request - no session ID or not initialization request + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, - message: 'Bad Request: No valid session ID provided' + message: 'Bad Request: Session ID required' }, id: null }); @@ -725,8 +740,14 @@ if (useOAuth && authMiddleware) { // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) const mcpGetHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } @@ -756,8 +777,14 @@ if (useOAuth && authMiddleware) { // Handle DELETE requests for session termination (according to MCP spec) const mcpDeleteHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 9092926d9..d0c8bf718 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -649,6 +649,16 @@ app.post('/mcp', async (req: Request, res: Response) => { if (sessionId && transports[sessionId]) { transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_000, message: 'Session not found' }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), @@ -671,9 +681,12 @@ app.post('/mcp', async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; } else { + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', - error: { code: -32_000, message: 'Bad Request: No valid session ID' }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -695,8 +708,14 @@ app.post('/mcp', async (req: Request, res: Response) => { // Handle GET requests for SSE streams app.get('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } @@ -707,8 +726,14 @@ app.get('/mcp', async (req: Request, res: Response) => { // Handle DELETE requests for session termination app.delete('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index b1b2ccf51..c500922b9 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -57,6 +57,19 @@ app.post('/mcp', async (req: Request, res: Response) => { if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; + } else if (sessionId && !transports[sessionId]) { + // Session ID provided but not found - return 404 to signal client should start new session + // Per MCP spec: "When a client receives HTTP 404 in response to a request containing + // an Mcp-Session-Id, it MUST start a new session" + res.status(404).json({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: 'Session not found' + }, + id: null + }); + return; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - create a fresh server for this client const server = getServer(); @@ -87,12 +100,14 @@ app.post('/mcp', async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled } else { - // Invalid request - no session ID or not initialization request + // No session ID and not an initialization request - return 400 + // Per MCP spec: "Servers that require a session ID SHOULD respond to requests without + // an Mcp-Session-Id header (other than initialization) with HTTP 400 Bad Request" res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, - message: 'Bad Request: No valid session ID provided' + message: 'Bad Request: Session ID required' }, id: null }); @@ -119,8 +134,14 @@ app.post('/mcp', async (req: Request, res: Response) => { // Handle GET requests for SSE streams (now using built-in support from StreamableHTTP) app.get('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); + if (!sessionId) { + // No session ID provided + res.status(400).send('Session ID required'); + return; + } + if (!transports[sessionId]) { + // Session ID provided but not found - return 404 per MCP spec + res.status(404).send('Session not found'); return; } From 64c7a4af4adb4e8196294065881e1e16b6748b91 Mon Sep 17 00:00:00 2001 From: Joe Wanko Date: Thu, 19 Mar 2026 00:47:42 -0400 Subject: [PATCH 2/2] chore: add changeset for invalid session ID fix --- .changeset/pink-goats-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pink-goats-push.md diff --git a/.changeset/pink-goats-push.md b/.changeset/pink-goats-push.md new file mode 100644 index 000000000..4e2b3c6b7 --- /dev/null +++ b/.changeset/pink-goats-push.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/examples-server": patch +--- + +fix(examples): return 404 for invalid session IDs