Skip to content

fix(server): serve draft-version requests statelessly per SEP-2567#999

Open
alexhancock wants to merge 3 commits into
mainfrom
alexhancock/sep-2567
Open

fix(server): serve draft-version requests statelessly per SEP-2567#999
alexhancock wants to merge 3 commits into
mainfrom
alexhancock/sep-2567

Conversation

@alexhancock

Copy link
Copy Markdown
Contributor

Motivation and Context

Per modelcontextprotocol/modelcontextprotocol#2567 protocol-level sessions (and the Mcp-Session-Id header) are removed from the Streamable HTTP transport as of the draft protocol version (2026-07-28); servers speaking that version MUST serve requests statelessly.

The rust-sdk server assigned an Mcp-Session-Id in its default stateful_mode even when the negotiated protocol version was 2026-07-28. This gates the session code path on the negotiated version: sessions are used only for pre-draft versions (< 2026-07-28), and draft-version requests are served statelessly even in stateful_mode. Older versions retain full session behavior.

How Has This Been Tested?

No dedicated SEP-2567 conformance scenario exists (the conformance PR that would add one was closed unmerged); per the maintainer, every existing draft (2026-07-28) scenario already sends sessionless requests. Verified against the existing draft server suite:

cargo build -p mcp-conformance
STATELESS= ./target/debug/conformance-server &   # default stateful_mode
npx -y @modelcontextprotocol/conformance@0.2.0-alpha.9 server --url http://127.0.0.1:8001/mcp --suite draft -o results

The default (stateful) server now produces results byte-for-byte identical to an explicit stateless server (48 passed / 31 failed; the 31 remaining failures are pre-existing SEP-2575 server/discover gaps, out of scope). rmcp lib unit tests (232) and streamable-HTTP integration tests (session store, stale session, protocol version, JSON response — 19) pass locally.

Breaking Changes

None. The session code path is unchanged for all pre-2026-07-28 versions; only the draft version changes behavior, bringing it into spec compliance.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

SEP-2575 (also in progress) removes the initialize handshake for the draft version. Once that lands, the version detection here can be simplified to a lifecycle check (an initialize request implies the pre-draft/stateful lifecycle; draft requests never carry initialize).

@alexhancock
alexhancock requested a review from a team as a code owner July 16, 2026 18:35
@github-actions github-actions Bot added T-core Core library changes T-transport Transport layer changes labels Jul 16, 2026
.unwrap_or(ProtocolVersion::V_2025_03_26)
});
let use_session =
self.config.stateful_mode && declared_version < ProtocolVersion::V_2026_07_28;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since #973 has landed, could we classify the request as legacy or modern before touching the session manager, then derive use_session from that classification instead of ordering ProtocolVersion? I'm thinking of something like stateful_mode && lifecycle.is_legacy(). This would also let malformed or unsupported versions fail before any session lookup or restore.

))
}

async fn handle_post<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET and DELETE still enter legacy session handlers?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since 2026-07-28 requests are always stateless, should we deprecate stateful_mode in favor of a legacy-scoped option such as legacy_session_mode? After this change, the current option only affects pre-2026-07-28 traffic, but its name suggests that it controls all requests. Since v3 already allows breaking changes, this may be a good opportunity to clarify the API.

@alexhancock
alexhancock force-pushed the alexhancock/sep-2567 branch from 78e4e11 to d27b509 Compare July 17, 2026 17:56
@github-actions github-actions Bot added T-documentation Documentation improvements T-test Testing related changes labels Jul 17, 2026
Comment on lines +1240 to +1242
if !is_legacy_request(None, request.headers()) {
return Ok(method_not_allowed_response());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this check to the start of handle_get() to make draft GET behavior independent of legacy SSE validation.

}

// SEP-2567: sessions are removed from 2026-07-28; older versions are legacy.
fn is_legacy_request(message: Option<&ClientJsonRpcMessage>, headers: &HeaderMap) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this helper return Result and perform the existing header/meta validation before any caller touches the session manager?

fn validate_protocol_version_header(

fn validate_header_matches_init_body(

fn validate_request_protocol_version_meta(

pub sse_retry: Option<Duration>,
/// If true, the server will create a session for each request and keep it alive.
/// When enabled, SSE priming events are sent to enable client reconnection.
pub stateful_mode: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a breaking change, the commit message should contains ! to pass CI like refactor!: rename stateful_mode to legacy_session_mode.

@DaleSeo DaleSeo linked an issue Jul 18, 2026 that may be closed by this pull request
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-documentation Documentation improvements T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-2567: Sessionless MCP via Explicit State Handles

2 participants