Repository navigation
Proposal: Add client state persistence API to allow MCP clients to survive restarts #376
Copy link
Copy link
Closed
Closed
Copy link
Labels
P1Significant bug affecting many users, highly requested featureSignificant bug affecting many users, highly requested featurearea/clientenhancementNew feature or requestNew feature or requestfeature/session-storagewontfixThis will not be worked onThis will not be worked on
Description
Motivation
Problem Statement
The current Java MCP SDK provides no mechanism for persisting client runtime state. Without SDK support, it is impossible for applications to resume a prior session, reuse an existing access token, or skip redundant protocol negotiation after a restart.
Example Failure Scenario
- Startup – An
McpClientsends aninitializerequest tohttps://example.com/mcp. - Session established – The server responds with
Mcp-Session-Id: 1868a90c…; the client continues issuing requests within that session. The server may create associated state (e.g., cached prompts, resources, intermediate results) tied to the session ID. - Crash / redeploy – The JVM crashes or the container restarts.
- Restart – The new client instance knows nothing about the previous session ID, access token, or negotiated metadata. As a result, it must repeat the OAuth flow and send a new
initializerequest, forcing the server to discard prior state and degrading the user experience.
Impact
Introducing a first-class SDK abstraction for state persistence will deliver:
- Improve reliability – Maintain continuity for long-running operations that depend on server-side session state
- Better user experience – Seamless resumption without re-authentication delays
- Production readiness – Essential for containerized environments with frequent deployments
- Operational simplicity – Let the host application decide where and how to store this data (e.g., files, JDBC, Redis, secret vault) while keeping the SDK storage-agnostic
Use Cases
This feature is particularly valuable for:
- Long-running applications that need to survive restarts without losing context
- Containerized environments with frequent deployments and rolling updates
- Enterprise applications where remote MCP servers are desirable because they are easier to manage across thousands of employees
- Batch processing systems that require session continuity across job restarts
Proposed API
Here is an example API to support discussions around this request.
package io.modelcontextprotocol.spec;
/** Wraps the OAuth 2.1 access token used in Authorization headers. */
public record TokenInfo(String accessToken) { }
/**
* Negotiated protocol version and server capabilities returned by the server.
*
* @param protocolVersion the agreed MCP protocol version
* @param capabilitiesJson server capabilities serialized as JSON (or any text format)
*/
public record ServerMetadata(String protocolVersion, String capabilitiesJson) { }package io.modelcontextprotocol.spec;
import java.util.Optional;
/**
* Persists MCP client state so a Streamable-HTTP client can resume seamlessly
* after a JVM restart. Implementations MUST be thread-safe.
*
* <p>Host applications remain in full control of where & how the data is stored.
*
* <p>Example usage:
*
* <pre>{@code
* StreamableHttpClientStateStore myStateStore = new MyJdbcBackedStateStore("my-client-key");
* var transport = HttpClientStreamableHttpTransport.builder("https://example.com/mcp")
* .clientStateStore(myStateStore)
* .build();
*
* McpSyncClient client = McpClient.sync(transport).build();
* }</pre>
*/
public interface StreamableHttpClientStateStore {
/* ------- Session ------- */
/**
* Called by the MCP client when a remote MCP server returns an MCP-Session-Id
* per the Streamable HTTP specification after an initialize request.
*
* @param the session ID returned by the server.
*/
void setSessionId(String sessionId);
/**
* Called by the MCP client before sending any requests to the server.
* If a value is present, it will be included in the Mcp-Session-Id header.
*
* @return an Optional containing the stored session ID if one was previously stored.
*/
Optional<String> getSessionId();
/**
* Called by the MCP client when the server indicates that the session has expired or is invalid,
* to clear the stored session ID.
*/
void clearSessionId();
/* ------- Access Token ------- */
/**
* Called by the MCP client after obtaining a new OAuth 2.1 access token from the authorization server.
*
* @param token the TokenInfo containing the new access token.
*/
void setAccessToken(TokenInfo token);
/**
* Called by the MCP client before sending any authorized requests to the server.
* If a value is present, it will be included in the Authorization header as a Bearer token.
*
* @return an Optional containing the stored access token if one was previously stored.
*/
Optional<TokenInfo> getAccessToken();
/**
* Called by the MCP client when the access token is revoked, expires, or is no longer valid,
* to clear the stored token.
*/
void clearAccessToken();
/* ------- Server Metadata ------- */
/**
* Called by the MCP client after initialization to store the negotiated
* protocol version and server capabilities for future reuse.
*
* @param metadata the ServerMetadata containing protocol version and capabilitiesJson.
*/
void setServerMetadata(ServerMetadata metadata);
/**
* Called by the MCP client before initialization to retrieve any previously stored
* protocol version and server capabilities.
*
* @return an Optional containing the stored ServerMetadata if one was previously stored.
*/
Optional<ServerMetadata> getServerMetadata();
/**
* Called by the MCP client to clear any stored server metadata,
* for example if the protocol version becomes incompatible.
*/
void clearServerMetadata();
}Design Rationale
| Aspect | Reasoning |
|---|---|
| Atomic value objects | strongly typed interfaces, ability to maybe add new fields if needed but we could drop these for just strings if we want to keep things super simple |
capabilitiesJson as String |
Simplest for JDBC / key-value stores—no schema required. |
| Storage-agnostic | Host application chooses file, DB, secret vault, etc.; SDK stays neutral. |
Implementation Details
Integration Point
The state store would be configured on the various transport builders for example HttpClientStreamableHttpTransport.Builder:
var transport = HttpClientStreamableHttpTransport.builder("https://example.com/mcp")
.clientStateStore(new FileBasedStateStore("./mcp-state.json"))
.build();Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P1Significant bug affecting many users, highly requested featureSignificant bug affecting many users, highly requested featurearea/clientenhancementNew feature or requestNew feature or requestfeature/session-storagewontfixThis will not be worked onThis will not be worked on
Activity
falcowinkler commentedon Jul 14, 2025
A configurable state store could also be great for replicated deployments.
ckipp01 commentedon Jul 30, 2025
We've encountered this exact issue in Metals (Scala language server) when using the Java MCP SDK.
Workflow where this occurs
Evidence
From our MCP trace logs (
.metals/mcp.trace.json), we can see the exact pattern:Impact
This makes MCP completely unusable in any development workflow that involves server restarts, which is very common during development. Users have to restart their entire Claude Code session to restore MCP functionality.
We've documented this in detail in our issue: scalameta/metals#7674
This confirms that the client state persistence API mentioned in this issue is critical for real-world usage where server restarts are a normal part of the development workflow.
feat: add McpSessionStore SPI for pluggable session storage
Kehrlann commentedon Sep 1, 2026
2026-07-28 spec makes servers stateless. We won't bring the API in older version of the spec, closing as
wont-fix.