Skip to content

feat: speak MCP 2026-07-28 without breaking older peers - #105

Merged
avelino merged 5 commits into
mainfrom
avelino/issue-104
Jul 31, 2026
Merged

feat: speak MCP 2026-07-28 without breaking older peers#105
avelino merged 5 commits into
mainfrom
avelino/issue-104

Conversation

@avelino

@avelino avelino commented Jul 30, 2026

Copy link
Copy Markdown
Owner

MCP 2026-07-28 drops the initialize handshake and protocol-level sessions, so every request now carries its own version and identity. We sit on both sides of the wire and every backend we proxy is still on 2025-11-25 or older, so picking one revision was never an option.

Version is negotiated per request. The client probes server/discover and falls back to the legacy handshake on any failure, including a backend that dies on the unknown method. The proxy answers both shapes and only emits resultType, cache hints and the new routing headers to a peer that declared the new revision, so a legacy client sees the same bytes as before. Authorization got the hardening that shipped with the revision. The AS emits iss, the client validates it byte exact, metadata issuers are checked against where they were fetched from, and client registrations are keyed by issuer with existing on-disk stores migrating in place.

subscriptions/listen, CIMD and the tasks extension stay out, tracked separately. DCR remains supported, it is deprecated with a 12 month window.

Fixes #104

MCP 2026-07-28 drops the initialize handshake and protocol-level sessions, so
every request now carries its own version and identity. We sit on both sides of
the wire and every backend we proxy is still on 2025-11-25 or older, so picking
one revision was never an option.

Version is negotiated per request. The client probes server/discover and falls
back to the legacy handshake on any failure, including a backend that dies on
the unknown method. The proxy answers both shapes and only emits resultType,
cache hints and the new routing headers to a peer that declared the new
revision, so a legacy client sees the same bytes as before. Authorization got
the hardening that shipped with the revision. The AS emits iss, the client
validates it byte exact, metadata issuers are checked against where they were
fetched from, and client registrations are keyed by issuer with existing
on-disk stores migrating in place.

subscriptions/listen, CIMD and the tasks extension stay out, tracked
separately. DCR remains supported, it is deprecated with a 12 month window.

Fixes #104

Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds dual-stack support for MCP 2026-07-28 while preserving byte-for-byte behavior for legacy peers/backends, including per-request protocol negotiation, Streamable HTTP request-metadata headers, MRTR relay-through, and OAuth-AS hardening/migration work needed by the new revision.

Changes:

  • Introduces per-request stateless semantics (server/discover, _meta-carried version/identity, resultType, cache hints) while keeping legacy initialize behavior intact.
  • Implements/validates Streamable HTTP metadata headers (Mcp-Method, Mcp-Name, MCP-Protocol-Version) and adds transport support for per-request Mcp-Param-* mirroring (x-mcp-header).
  • Hardens OAuth AS/client registration handling (RFC 9207 iss, SEP-837 application_type, issuer-keyed client registration storage with on-disk migration), plus extensive e2e coverage for the seam and HTTP contract.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/transport/mod.rs Extends the Transport trait with per-request header mirroring and negotiated protocol version signaling.
src/transport/http.rs Implements stateless-vs-legacy HTTP behavior, request-metadata headers, Mcp-Param-* mirroring, and regression tests for wire headers across revisions.
src/server_auth/oauth_as/register.rs Accepts and conditionally echoes DCR application_type per SEP-837/RFC 7591; adds tests for compatibility and validation.
src/server_auth/oauth_as/e2e_tests.rs Adds an end-to-end assertion that iss on the authorize redirect matches advertised issuer.
src/server_auth/oauth_as/authorize.rs Adds RFC 9207 iss to authorization redirects with normalization + unit tests.
src/serve/stdio.rs Plumbs per-request stateless detection into stdio serving path (no headers available).
src/serve/proxy.rs Adds server/discover, initialize version echoing, MRTR continuation passthrough, and backend-param rewriting helpers + tests.
src/serve/mod.rs Wires new serve e2e test modules for seam + conformance coverage.
src/serve/http.rs Validates routing/version headers, returns spec-pinned HTTP status codes for modern peers, and adds router support for integration tests.
src/serve/e2e_tests.rs New seam e2e suite covering negotiation, legacy compatibility, routing headers, MRTR relay, and x-mcp-header mirroring through the proxy.
src/serve/e2e_conformance_tests.rs New end-to-end socket tests pinning /mcp HTTP status + header contract and server/discover wire shape.
src/protocol.rs Updates protocol constants/version sets, adds header sentinel encoding/decoding, stateless metadata helpers, cache hints, and new protocol structs.
src/cli/server.rs Adds optional proxy routing via MCP_PROXY_URL, strips/rewrites namespacing in listings/calls, and reports negotiated protocol version in JSON health output.
src/auth/store.rs Migrates auth-store schema to issuer-keyed client registrations with legacy fallback and extensive migration/behavior tests.
docs/reference/environment-variables.md Documents the special 3s server/discover probe timeout behavior.
docs/reference/cli.md Documents --health and JSON protocolVersion, including proxy caveat.
docs/reference/architecture.md Updates architecture docs for discovery-first negotiation, stateless headers, and issuer-keyed credential storage.
docs/howto/oauth-as.md Documents iss, issuer validation, issuer-keyed registration storage + migration, and SEP-837 behavior.
docs/guides/proxy-mode.md Updates proxy-mode docs for multi-revision behavior, header validation, and HTTP+SSE deprecation warning behavior.
docs/guides/configuration.md Updates HTTP transport docs for revision-dependent session handling and routing headers.
.gitignore Ignores .claude/worktrees/.

Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 30, 2026 23:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 24 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/auth/store.rs:130

  • AuthStore::set_client can persist an issuer-keyed registration while leaving version at 0 if the store was constructed via AuthStore::default() (or otherwise not passed through migrate). On the next load_auth_store(), migrate will treat it as v0 and move clients into legacy_clients, effectively demoting issuer-keyed registrations. Setting version here makes the invariant self-contained and prevents accidental re-demotion.
    pub fn set_client(&mut self, issuer: &str, server_key: &str, reg: ClientRegistration) {
        self.legacy_clients.remove(server_key);
        self.clients.insert(issuer_key(issuer), reg);
    }

src/serve/http.rs:129

  • check_routing_headers treats a present-but-non-UTF8 routing header as if it were absent (to_str().ok()), which contradicts the comment above about rejecting invalid header characters. This can let a gateway-route/meter claim go unvalidated in the one case where the header is malformed. Consider explicitly rejecting any present routing header whose value fails to_str().
fn check_routing_headers(headers: &HeaderMap, req: &JsonRpcRequest) -> Option<String> {
    let header = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());

avelino added 3 commits July 30, 2026 21:25
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 31, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/serve/http.rs:207

  • check_protocol_version_header currently ignores an MCP-Protocol-Version header whose value can’t be decoded via HeaderValue::to_str() (it becomes None and the header is treated as absent). That makes malformed header bytes silently bypass the unsupported-version check and can lead to confusing legacy-vs-stateless behavior selection. It’s safer to treat a present-but-invalid header value as a -32020 HeaderMismatch (400).
    let declared = headers
        .get(crate::protocol::HEADER_MCP_PROTOCOL_VERSION)
        .and_then(|v| v.to_str().ok())?;
    if crate::protocol::is_version_supported(declared) {

src/serve/http.rs:126

  • check_routing_headers treats a routing header whose value contains non-ASCII/invalid bytes as if the header were absent because it uses to_str().ok(). That means a malformed Mcp-Method / Mcp-Name can bypass mismatch validation entirely, even though the spec calls out invalid header characters as a rejectable condition. Consider treating non-decodable header values as an immediate mismatch instead of silently ignoring them.

This issue also appears on line 204 of the same file.

fn check_routing_headers(headers: &HeaderMap, req: &JsonRpcRequest) -> Option<String> {
    let header = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());

@avelino
avelino merged commit e795cdc into main Jul 31, 2026
10 checks passed
@avelino
avelino deleted the avelino/issue-104 branch July 31, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support MCP spec 2026-07-28 without breaking 2025-11-25 clients and backends

2 participants