-
Notifications
You must be signed in to change notification settings - Fork 579
Open
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
McpClient
sends aninitialize
request 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
initialize
request, 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();
falcowinkler, okohub and reflek-chris
Metadata
Metadata
Assignees
Labels
No labels