yikart/AiToEarn · error

-32000

-32000

Error message

Bad Request: Mcp-Session-Id header is required

What it means

In stateful mode, a POST arrived with no Mcp-Session-Id header and the body was not an initialize request. Since only initialize may create a session, the server cannot route the call and returns -32000 with HTTP 400. Every non-initialize stateful request must reference an existing session.

Source

Thrown at project/aitoearn-backend/libs/nest-mcp/src/services/mcp-streamable-http.service.ts:313

          id: null,
        })
        return
      }

      // Use existing transport
      const transport = this.transports[sessionId]

      this.logger.debug(
        `[${sessionId}] Handling request with existing session`,
      )

      // Handle the request with existing transport and handlers
      await transport.handleRequest(req.raw, res.raw, body)
      return
    }

    // Case 3: No session ID and not initialization
    res.status(400).json({
      jsonrpc: '2.0',
      error: {
        code: -32000,
        message: 'Bad Request: Mcp-Session-Id header is required',
      },
      id: null,
    })
  }

  /**
   * Handle GET requests for SSE streams
   */
  async handleGetRequest(req: any, res: any): Promise<void> {
    const adapter = HttpAdapterFactory.getAdapter(req, res)
    const adaptedReq = adapter.adaptRequest(req)
    const adaptedRes = adapter.adaptResponse(res)

    if (this.isStatelessMode) {

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Send an initialize request first, store the Mcp-Session-Id response header, and attach it to all subsequent requests.
  2. If your usage is one-shot calls without sessions, use the library's stateless mode (isStatelessMode) instead.
  3. Audit your HTTP client so the mcp-session-id header persists across every request in the conversation.
  4. Check that middleware or the proxy isn't stripping the Mcp-Session-Id header.

Example fix

// before
await fetch(url, { method: 'POST', body: JSON.stringify(callToolReq) })
// after
const initRes = await fetch(url, { method: 'POST', body: JSON.stringify(initReq) })
const sid = initRes.headers.get('mcp-session-id')
await fetch(url, { method: 'POST', headers: { 'mcp-session-id': sid }, body: JSON.stringify(callToolReq) })
Defensive patterns

Strategy: validation

Validate before calling

if (!sessionId) {
  throw new Error('Call initialize first and pass the returned Mcp-Session-Id header')
}

Type guard

function isStatefulReady(ctx: { sessionId?: string; initialized: boolean }): boolean {
  return ctx.initialized && typeof ctx.sessionId === 'string' && ctx.sessionId.length > 0
}

Try / catch

try {
  return await post(url, body, { 'mcp-session-id': sessionId })
} catch (e) {
  if (e.status === 400 && e.body?.error?.code === -32000) {
    sessionId = await initialize(url)
    return post(url, body, { 'mcp-session-id': sessionId })
  }
  throw e
}

Prevention

When it happens

Trigger: POSTing tools/list, tools/call, resources/*, or notifications without initializing first (no session id header), or losing the header between requests because the HTTP client drops custom headers on subsequent calls.

Common situations: Calling a tool directly with curl without doing the initialize dance first, fetch/axios wrapper that rebuilds headers per request and forgets mcp-session-id, or misconfiguring the client to talk to a stateful endpoint when it assumes stateless behavior.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/9c221b5dbdd76148. Report an issue: GitHub.