feat(mcp): resume downstream MCP sessions on any replica

Draft
SA
3d ago
+1137-46

Description

  • An Mcp-Session-Id issued 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 initialize writes the session's binding to the core shared store (mcp.sessions namespace) before the response carries the ID: auth key ID, user path, endpoint, X-MCP-Servers scope, 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 Stateless drops sessions altogether. It does export StreamableServerTransport and Server.Connect with ServerSessionOptions.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-ID replay 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.