yikart/AiToEarn · error

MCP server not found for session

Error message

MCP server not found for session

What it means

After finding a transport, the SSE service looks up the associated McpServer instance for the same sessionId. If the transport exists but no server is registered, it responds 404 'MCP server not found for session'. This indicates partial internal state: the two maps (transports and mcpServers) are out of sync.

Source

Thrown at project/aitoearn-backend/libs/nest-mcp/src/services/mcp-sse.service.ts:115

  }

  /**
   * Handle message processing for SSE
   */
  async handleMessage(rawReq: any, rawRes: any, body: unknown): Promise<any> {
    const adapter = HttpAdapterFactory.getAdapter(rawReq, rawRes)
    const req = adapter.adaptRequest(rawReq)
    const res = adapter.adaptResponse(rawRes)
    const sessionId = req.query['sessionId'] as string
    const transport = this.transports.get(sessionId)

    if (!transport) {
      return res.status(404).send('Session not found')
    }

    const mcpServer = this.mcpServers.get(sessionId)
    if (!mcpServer) {
      return res.status(404).send('MCP server not found for session')
    }

    // Resolve the request-scoped tool executor service
    const contextId = ContextIdFactory.getByRequest(req)
    const executor = await this.moduleRef.resolve(
      McpExecutorService,
      contextId,
    )

    // Register request handlers with the user context from this specific request
    executor.registerRequestHandlers(mcpServer, req)

    // Process the message
    await transport.handlePostMessage(req.raw, res.raw, body)
  }
}

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Close and re-open the SSE session so transport and server are created together
  2. Make transport and McpServer registration/disposal atomic (register both together, clean both together on failure)
  3. Add cleanup on transport close to remove both entries, preventing stale transports
  4. Log and correlate sessionId lifecycle events to find where the maps diverge

Example fix

// before
this.transports.set(sessionId, transport)
await this.startServer(sessionId) // if this throws, transport leaks
// after
try {
  const server = await this.startServer(sessionId)
  this.transports.set(sessionId, transport)
  this.mcpServers.set(sessionId, server)
} catch (e) {
  transport.close()
  throw e
}
Defensive patterns

Strategy: fallback

Validate before calling

// client cannot detect this in advance; on the server, guard lifecycle:
// assert(this.mcpServers.has(sessionId), 'server must be registered with transport')

Type guard

function isServerMissing(resp: { status: number; body?: unknown }): boolean {
  return resp.status === 404 && typeof resp.body === 'string' && resp.body.includes('MCP server not found')
}

Try / catch

try {
  await postMessage(sessionId, payload)
} catch (e) {
  if (isServerMissingErr(e)) {
    await forceCloseSession(sessionId)   // clears desynced transport
    sessionId = await reconnectSse()
    await postMessage(sessionId, payload)
  } else { throw e }
}

Prevention

When it happens

Trigger: POST with a sessionId present in transports but absent from mcpServers — e.g., server registration failed or was cleaned up while the transport leaked, or state maps updated non-atomically during connect/disconnect.

Common situations: Error during session initialization after transport creation; cleanup routine removing mcpServers entries but not transports; race between concurrent connect/disconnect; hot reload or partial restart clearing one map.

Related errors


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