spring-projects / spring-projects/spring-ai
WebMvcStreamableServerTransportProvider: expose SessionStore SPI for cloud-native deployments
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Problem
WebMvcStreamableServerTransportProvider (autoconfigured when spring.ai.mcp.server.protocol=STREAMABLE via spring-ai-starter-mcp-server-webmvc) keeps its Mcp-Session-Id → McpStreamableServerSession map in JVM memory as a private field with no extension point.
For any horizontally autoscaled deployment — Cloud Run, ECS, Kubernetes HPA, autoscaling VM groups — this is fundamentally fragile. Every time an instance is recycled (idle scale-to-zero, deploy, max-CPU eviction, crash, regional failover), all sessions held by that JVM disappear. Clients that retry with a previously-issued Mcp-Session-Id receive 404 Session not found from the transport until they reconnect.
Real-world incident
Production (Spring AI 1.1.2, mcp-spring-webmvc 0.17.0 transitively) on Google Cloud Run.
- Client (Claude Desktop via mcp-remote) opens an MCP session, receives an
Mcp-Session-Id. - Client idles for ~4.5 hours.
- Cloud Run scales the instance to zero (default behavior).
- Client sends
tools/callwith the sameMcp-Session-Id. - Cloud Run spins up a fresh instance with an empty session map → 404.
- Client retries against more fresh instances → 404 → 404 → 404 until the client's internal request timeout fires.
We mitigated short-term with --min-instances=1 (defeats idle scale-to-zero) and --session-affinity (best-effort cookie routing) but neither defends against deploys, crashes, or scale-out from load.
Why STATELESS is not a workaround for us
STATELESS would avoid this entirely by removing Mcp-Session-Id from the wire. But Spring AI 1.1.x BOM pins mcp-core 0.17.0, which rejects the elicitation: { form: {} } payload that current MCP clients (Cursor, Claude Code 2.1.74+, Antigravity, VS Code Copilot) send per spec 2025-11-25 with Unrecognized field "form". Filed separately as #.
Proposed solution
Expose a pluggable SessionStore SPI so users can back the session map with whatever is appropriate for their deployment topology — JDBC, Redis, Memcached, etc. Default implementation stays the current in-memory ConcurrentHashMap, preserving backward compatibility.
Sketch:
public interface McpStreamableSessionStore {
Optional<McpStreamableServerSession> get(String sessionId);
void put(String sessionId, McpStreamableServerSession session);
void remove(String sessionId);
// optional TTL hook
default void expireOlderThan(Instant cutoff) {}
}
WebMvcStreamableServerTransportProvider.Builder would gain .sessionStore(McpStreamableSessionStore) defaulting to the in-memory implementation.
Acceptance criteria
-
McpStreamableSessionStoreinterface exists inmcp-spring-webmvc(and the WebFlux counterpart). -
WebMvcStreamableServerTransportProvider.Builder.sessionStore(...)accepts a custom store. - Spring Boot autoconfigure picks up a user-defined
McpStreamableSessionStorebean if present, otherwise falls back to the in-memory default. - Documentation example showing a JDBC-backed store (similar to Spring Session).
- No behavior change for users who don't configure a custom store.
Environment
- Spring Boot 3.5.11
- Spring AI 1.1.2 (also reproduced behavior with 1.1.5)
mcp-core0.17.0 (BOM transitive)- Deployment: Google Cloud Run,
min-instances=0..10,concurrency=80
Related
- #5411 — STATELESS reconnect 404 against AWS Bedrock AgentCore (different angle, same class of bug)
- #2740 — SSE clients fail to reconnect after server restart
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing WebMvcStreamableServerTransportProvider.Builder and the Spring Boot autoconfiguration in mcp-spring-webmvc, then compare the WebFlux counterpart. Review how the current in-memory session map is used and how custom beans are discovered. Done means both transports expose the SPI, autoconfiguration preserves the default, and the documentation includes a JDBC-backed example.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring-boot
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100