spring-projects / spring-projects/spring-ai

WebMvcStreamableServerTransportProvider: expose SessionStore SPI for cloud-native deployments

Open
#5,983 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
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.

  1. Client (Claude Desktop via mcp-remote) opens an MCP session, receives an Mcp-Session-Id.
  2. Client idles for ~4.5 hours.
  3. Cloud Run scales the instance to zero (default behavior).
  4. Client sends tools/call with the same Mcp-Session-Id.
  5. Cloud Run spins up a fresh instance with an empty session map → 404.
  6. 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
  • McpStreamableSessionStore interface exists in mcp-spring-webmvc (and the WebFlux counterpart).
  • WebMvcStreamableServerTransportProvider.Builder.sessionStore(...) accepts a custom store.
  • Spring Boot autoconfigure picks up a user-defined McpStreamableSessionStore bean 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-core 0.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.