Skip to content

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

Open
alexhancock wants to merge 1 commit into
mainfrom
alexhancock/sep-2567
Open

fix(server): serve draft-version requests statelessly per SEP-2567#999
alexhancock wants to merge 1 commit 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.

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-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants