From 15e5b367a6b39689a308e3dda3b495320aa35e66 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 26 Mar 2026 17:12:09 +0000 Subject: [PATCH] fix(examples): return 404 for unknown session IDs, 400 for missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per spec, clients interpret 404 as "start a new session" — the examples were returning 400 for both missing and unknown session IDs, which breaks client recovery logic. Split the check so unknown IDs return 404. Fixes #389 --- .changeset/fix-session-status-codes.md | 5 ++++ examples/server/src/elicitationFormExample.ts | 29 +++++++++++++------ examples/server/src/elicitationUrlExample.ts | 29 +++++++++++++------ .../server/src/jsonResponseStreamableHttp.ts | 13 +++++---- examples/server/src/simpleStreamableHttp.ts | 29 +++++++++++++------ examples/server/src/simpleTaskInteractive.ts | 25 ++++++++++++---- .../src/standaloneSseWithGetStreamableHttp.ts | 21 +++++++++----- test/conformance/src/authTestServer.ts | 28 +++++++++++++----- test/conformance/src/everythingServer.ts | 28 +++++++++++++----- 9 files changed, 147 insertions(+), 60 deletions(-) create mode 100644 .changeset/fix-session-status-codes.md diff --git a/.changeset/fix-session-status-codes.md b/.changeset/fix-session-status-codes.md new file mode 100644 index 000000000..ff2a264bf --- /dev/null +++ b/.changeset/fix-session-status-codes.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/examples-server': patch +--- + +Example servers now return HTTP 404 (not 400) when a request includes an unknown session ID, so clients can correctly detect they need to start a new session. Requests missing a session ID entirely still return 400. diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index 9c13b739e..e059e8452 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -364,14 +364,17 @@ async function main() { await transport.handleRequest(req, res, req.body); return; + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Bad Request: No valid session ID provided' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -399,8 +402,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -414,8 +421,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index c38dd75e8..462252deb 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -606,14 +606,17 @@ const mcpPostHandler = async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Bad Request: No valid session ID provided' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -643,8 +646,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -682,8 +689,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 7a3aad67a..01759d6fc 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -110,14 +110,17 @@ app.post('/mcp', async (req: Request, res: Response) => { await server.connect(transport); await transport.handleRequest(req, res, req.body); return; // Already handled + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Bad Request: No valid session ID provided' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index 1263f4bb5..1fc1522a1 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -689,14 +689,17 @@ const mcpPostHandler = async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); return; // Already handled + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Bad Request: No valid session ID provided' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -730,8 +733,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -761,8 +768,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 9092926d9..fc0d7280c 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -670,10 +670,17 @@ app.post('/mcp', async (req: Request, res: Response) => { await server.connect(transport); await transport.handleRequest(req, res, req.body); return; + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { 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 +702,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -707,8 +718,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index b1b2ccf51..7e133f6d2 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -86,14 +86,17 @@ app.post('/mcp', async (req: Request, res: Response) => { // Handle the request - the onsessioninitialized callback will store the transport await transport.handleRequest(req, res, req.body); return; // Already handled + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Bad Request: No valid session ID provided' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -119,8 +122,12 @@ 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/test/conformance/src/authTestServer.ts b/test/conformance/src/authTestServer.ts index c38a9afb0..56bf6092f 100644 --- a/test/conformance/src/authTestServer.ts +++ b/test/conformance/src/authTestServer.ts @@ -327,13 +327,17 @@ async function startServer() { await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); return; + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Invalid or missing session ID' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -359,8 +363,12 @@ async function startServer() { app.get('/mcp', bearerAuth, 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -381,8 +389,12 @@ async function startServer() { app.delete('/mcp', bearerAuth, 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index 25a6feb0f..f3925aeea 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -929,13 +929,17 @@ app.post('/mcp', async (req: Request, res: Response) => { await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); return; + } else if (sessionId) { + res.status(404).json({ + jsonrpc: '2.0', + error: { code: -32_001, message: 'Session not found' }, + id: null + }); + return; } else { res.status(400).json({ jsonrpc: '2.0', - error: { - code: -32_000, - message: 'Invalid or missing session ID' - }, + error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null }); return; @@ -961,8 +965,12 @@ app.post('/mcp', async (req: Request, res: Response) => { 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; } @@ -988,8 +996,12 @@ app.get('/mcp', async (req: Request, res: Response) => { 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) { + res.status(400).send('Missing session ID'); + return; + } + if (!transports[sessionId]) { + res.status(404).send('Session not found'); return; }