feat(mcp): resume downstream MCP sessions on any replica
Draft
SA
3d ago+1137+1137+0123456789012345678901234567890123456789-46-46-01234567890123456789
Description
- An
Mcp-Session-Idissued by one replica now works on every replica on the same PostgreSQL or MongoDB database, so MCP clients no longer need sticky sessions. - The replica that answers
initializewrites the session's binding to the core shared store (mcp.sessionsnamespace) before the response carries the ID: auth key ID, user path, endpoint,X-MCP-Serversscope, discovery mode, the client's initialize parameters, and the negotiated protocol version. - go-sdk v1.8.0 cannot adopt a session it did not create. Its handler keeps sessions in a private map with no store hook, and
Statelessdrops sessions altogether. It does exportStreamableServerTransportandServer.ConnectwithServerSessionOptions.State, so a replica that gets an unknown ID rebuilds the session from its record. It then serves that session's GET/POST/DELETE itself, with the SDK handler's request checks and idle timeout. Sessions a replica creates still go through the SDK handler unchanged. - Security model: the record is keyed by a SHA-256 digest of the session ID and holds no secrets. Every replica checks auth key, user path, and endpoint against it before resuming or serving, so a leaked ID under another identity gets 404 everywhere. A rejected request does not create a session.
- DELETE writes a close mark and deletes the record. Deleting a virtual server writes an endpoint close mark with a timestamp, so its sessions end on every replica and a virtual server recreated under the same name does not revive them. A replica serving a session rereads the close marks at most every 2 s, and extends the record's 30-minute expiry at most once a minute. Most requests touch no database.
- Survives a replica switch: tool calls, listings, prompts, resources, scope and discovery mode, negotiated protocol version, client info and capabilities, and the principal binding.
- Does not survive: a response stream in flight on a replica that stops (the client sees that request fail), and the open GET stream, which stays on its replica until the client reconnects it.
Last-Event-IDreplay was never supported. The gateway sends no sampling, elicitation, or roots requests, so none can be stranded. - Fallback: with SQLite, or when the shared store cannot be written at
initialize, sessions stay local as before. When a store read fails, a session this replica already serves keeps working, and a session it would have to resume answers 404, so the client re-initializes. - The core shared store is now also created when the MCP gateway runs on PostgreSQL or MongoDB.
- Docs: the MCP row of the replica table in production.mdx, and a new "Sessions across replicas" section in the MCP gateway page.
Tests: two Service instances share one store on SQLite, PostgreSQL, and MongoDB. Covered: resume with tools/list and tools/call, foreign key/user path/endpoint rejection, DELETE from either side, and virtual server deletion and recreation. Idle expiry and the bounded touch rate run against a clock-driven store. Plus the no-store fallback and the storage-type switch.