Skip to content

fix(server): negotiate initialize as legacy lifecycle - #1171

Open
AprilNEA wants to merge 1 commit into
modelcontextprotocol:mainfrom
AprilNEA:fix/legacy-initialize-negotiation
Open

fix(server): negotiate initialize as legacy lifecycle#1171
AprilNEA wants to merge 1 commit into
modelcontextprotocol:mainfrom
AprilNEA:fix/legacy-initialize-negotiation

Conversation

@AprilNEA

Copy link
Copy Markdown

Treat an initialize opener as selecting the legacy lifecycle, including when the client offers 2026-07-28. Negotiate only supported legacy revisions and reject initialize only when the server supports no legacy revision.

Motivation and Context

The 2026-07-28 protocol replaces the initialize handshake with the discover lifecycle. However, a client that still opens with initialize is explicitly selecting legacy lifecycle semantics.

Previously, a dual-era server could echo 2026-07-28 from initialize, even though that revision does not use the initialize lifecycle. Rejecting every such offer would also unnecessarily break clients capable of using an older revision.

This change makes a dual-era server negotiate the offer down to its preferred supported legacy revision (2025-11-25 by default). A modern-only server, with no supported legacy revision, returns Method Not Found.

The same rule is applied across direct transports, stateless HTTP handling, and stateful Streamable HTTP session classification. Discover/per-request modern requests remain sessionless.

How Has This Been Tested?

The following focused tests pass locally:

  • cargo test -p rmcp --test test_protocol_version_negotiation --features client
  • cargo test -p rmcp --test test_stateless_protocol_version --features 'transport-streamable-http-server reqwest'
  • cargo test -p rmcp --test test_client_lifecycle_modes --features client high_level_server_accepts_discover_startup_without_initialize
  • cargo test -p rmcp --test test_subscriptions_streamable_http --features 'client transport-streamable-http-client-reqwest transport-streamable-http-server reqwest' modern_http_lifecycle_stays_sessionless_for_older_application_version
  • cargo clippy -p rmcp --lib --features client -- -D warnings
  • cargo fmt --all --check
  • git diff --check

The behavior was also exercised in MCPlex's Streamable HTTP gateway integration test:

  • initialize(protocolVersion = 2026-07-28) opens a legacy session and negotiates 2025-11-25.
  • Requests carrying complete modern per-request metadata remain stateless.
  • The complete MCPlex test suite and clippy checks pass.

Breaking Changes

No API changes are required for users.

This corrects protocol negotiation behavior: an initialize request can no longer negotiate 2026-07-28. Servers supporting legacy revisions negotiate an appropriate legacy revision; modern-only servers reject the obsolete method.

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

The lifecycle is selected by how the client opens:

  • initialize selects legacy lifecycle semantics, regardless of the offered revision.
  • server/discover or a request carrying complete modern _meta selects the modern stateless lifecycle.

ServerHandler::supported_protocol_versions continues to advertise all supported revisions through discovery, while initialize negotiation considers only revisions older than 2026-07-28.

@AprilNEA
AprilNEA requested a review from a team as a code owner August 13, 2026 10:50
Copilot AI lite review requested due to automatic review settings August 13, 2026 10:50
@github-actions github-actions Bot added T-test Testing related changes T-core Core library changes T-handler Handler implementation changes T-service Service layer changes T-transport Transport layer changes labels Aug 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adjusts MCP protocol negotiation so an initialize opener always selects the legacy lifecycle (even when the client offers the modern 2026-07-28 revision), negotiating down to a supported legacy revision when possible and rejecting initialize only when the server supports no legacy revisions. This aligns direct transports and streamable HTTP behavior with the intended dual-era semantics.

Changes:

  • Update initialize protocol negotiation to consider only legacy revisions and return method not found when no legacy revision is supported.
  • Ensure streamable HTTP request classification treats initialize as legacy lifecycle regardless of offered version.
  • Expand/adjust tests to cover modern offers being negotiated down and modern-only servers rejecting initialize.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/rmcp/tests/test_stateless_protocol_version.rs Updates/extends streamable HTTP tests to validate modern-offer negotiation-down and modern-only rejection behavior.
crates/rmcp/tests/test_protocol_version_negotiation.rs Extends direct transport tests to verify modern initialize offers negotiate down to the preferred legacy revision.
crates/rmcp/src/transport/streamable_http_server/tower.rs Applies legacy negotiation result to stateless HTTP initialize handling and treats initialize as selecting legacy lifecycle for classification.
crates/rmcp/src/service/server.rs Changes negotiation function to return Option and rejects initialize when no legacy protocol is supported; updates handshake path accordingly.
crates/rmcp/src/service.rs Updates docs to clarify how supported versions bound legacy initialize negotiation vs discover lifecycle selection.
crates/rmcp/src/handler/server.rs Updates default ServerHandler::initialize to reject initialize when no legacy version can be negotiated.
Suppressed comments (1)

crates/rmcp/tests/test_stateless_protocol_version.rs:209

  • Same issue as the JSON variant: this loop only skips exactly 2026-07-28, so it may fail when newer modern revisions are added. Skip >= 2026-07-28 to keep the test aligned with legacy-only initialize negotiation.
        if version == &ProtocolVersion::V_2026_07_28 {
            continue;
        }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 492 to +496
tracing::warn!(
client_requested = %client_requested,
server_fallback = %server_fallback,
"client requested unsupported protocol version; falling back to server default"
"client requested a version unavailable to initialize; falling back to the server's preferred legacy version"
);
Comment on lines +188 to +190
if version == &ProtocolVersion::V_2026_07_28 {
continue;
}
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-handler Handler implementation changes T-service Service layer changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants