Skip to content

Proposal: Add client state persistence API to allow MCP clients to survive restarts #376

@asaikali

Description

@asaikali

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

  1. Startup – An McpClient sends an initialize request to https://example.com/mcp.
  2. 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.
  3. Crash / redeploy – The JVM crashes or the container restarts.
  4. 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();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions