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
- Close and re-open the SSE session so transport and server are created together
- Make transport and McpServer registration/disposal atomic (register both together, clean both together on failure)
- Add cleanup on transport close to remove both entries, preventing stale transports
- 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
- Register transport and McpServer atomically in one code path
- Dispose both maps together when a connection closes
- Fail fast if server init errors instead of leaving a transport behind
- Add invariant logging for sessionId presence across both maps
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
- Session not found
- -32001
- ResponseCode.ChannelAuthSessionInvalid
- ResponseCode.ChannelAuthPlatformMismatch
- ResponseCode.ChannelAuthSessionCompleted
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/0f7b1ae1c223971a.
Report an issue: GitHub.