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 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; }