linshenkx/prompt-optimizer · error

Invalid or missing session ID

Error message

Invalid or missing session ID

What it means

The GET /mcp endpoint (server-to-client SSE notifications) requires an mcp-session-id header matching a transport in the in-memory transports map. A missing header or an unknown/expired session ID yields HTTP 400 with this plain-text body.

Source

Thrown at packages/mcp-server/src/index.ts:443

            jsonrpc: '2.0',
            error: {
              code: -32000,
              message: 'Bad Request: No valid session ID provided',
            },
            id: null,
          });
          return;
        }

        // 处理请求
        await httpTransport.handleRequest(req, res, req.body);
      });

      // 处理 GET 请求(服务器到客户端通知,通过 SSE)
      app.get('/mcp', async (req, res) => {
        const sessionId = req.headers['mcp-session-id'] as string | undefined;
        if (!sessionId || !transports[sessionId]) {
          res.status(400).send('Invalid or missing session ID');
          return;
        }

        const httpTransport = transports[sessionId];
        await httpTransport.handleRequest(req, res);
      });

      // 处理 DELETE 请求(会话终止)
      app.delete('/mcp', async (req, res) => {
        const sessionId = req.headers['mcp-session-id'] as string | undefined;
        if (!sessionId || !transports[sessionId]) {
          res.status(400).send('Invalid or missing session ID');
          return;
        }

        const httpTransport = transports[sessionId];
        await httpTransport.handleRequest(req, res);
      });

View on GitHub (pinned to 3e677b1d9f)

Solutions

  1. Re-run the initialize handshake to obtain a fresh session ID before opening the SSE stream.
  2. If running multiple instances, use sticky sessions or a shared/external session store for transports.
  3. Detect 400 on the SSE GET and automatically re-initialize + resubscribe.

Example fix

// before
const es = new EventSource(MCP_URL + '/mcp')
// after
const es = new EventSource(MCP_URL + '/mcp', { } ) // EventSource cannot set headers; use fetch-based SSE:
// const res = await fetch(MCP_URL + '/mcp', { headers: { 'mcp-session-id': sessionId } })
Defensive patterns

Strategy: retry

Validate before calling

if (!sessionId || !(await sessionExists(sessionId))) {
  sessionId = await initializeSession() // handshake returns fresh mcp-session-id
}
const sseRes = await fetch(MCP_URL + '/mcp', { headers: { 'mcp-session-id': sessionId, accept: 'text/event-stream' } })
if (sseRes.status === 400) { sessionId = await initializeSession(); /* retry once */ }

Type guard

const isInvalidSessionResponse = (res: Response): boolean =>
  res.status === 400 && /Invalid or missing session ID/.test(res.text as unknown as string ?? '')

Try / catch

try { await openNotificationStream(sessionId) } catch { const fresh = await initializeSession(); await openNotificationStream(fresh) }

Prevention

When it happens

Trigger: GET /mcp with no mcp-session-id header, with a session ID from before a server restart, or after the session was deleted via DELETE /mcp; long-lived SSE clients that never re-initialize.

Common situations: Server process restarted (transports map is memory-only and empty), horizontal scaling where the SSE request lands on a different instance that doesn't hold the session, or clients that open the notification stream before completing initialize.

Related errors


AI-assisted analysis of linshenkx/prompt-optimizer@3e677b1d9f (2026-08-27). Data as JSON: /api/errors/26512165e30fb2a8. Report an issue: GitHub.