Add opt-in strict MCP-Protocol-Version validation - #5957
Merged
Merged
Conversation
JAORMX
requested review from
ChrisJBurns,
amirejaz,
aponcedeleonch,
blkt,
jhrozek,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
July 24, 2026 06:12
The streamable HTTP proxy accepted any MCP-Protocol-Version header (isSupportedMCPVersion always returned true). This was an undocumented, deliberately permissive stance: defensible for a transport-level proxy that does not depend on a specific MCP revision, but the spec says a server MUST reject an invalid/unsupported version with HTTP 400, and operators had no way to opt into that behavior. Add a --strict-protocol-validation flag (default off, preserving the version-agnostic behavior byte-for-byte). When enabled, the proxy rejects a request whose MCP-Protocol-Version header names an unknown MCP revision with HTTP 400; an absent header is still accepted (the spec says to assume 2025-03-26). The known-version set is checked against a small map, with the upcoming stateless revision sourced from mcp.MCPVersionModern so the strict gate and the ClassifyRevision routing path cannot drift. The flag is threaded end-to-end mirroring TrustProxyHeaders: CLI -> RunConfig -> types.Config -> StdioTransport -> streamable proxy option, and is preserved across thv upgrade via the config applier. The default version-agnostic stance and the opt-in flag are documented in docs/arch/03-transport-architecture.md. Closes #5764. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JAORMX
force-pushed
the
strict-mcp-protocol-validation-5764
branch
from
July 24, 2026 07:19
58faad1 to
402c013
Compare
rdimitrov
approved these changes
Jul 24, 2026
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5957 +/- ##
=======================================
Coverage 71.82% 71.83%
=======================================
Files 708 708
Lines 72798 72811 +13
=======================================
+ Hits 52287 52302 +15
Misses 16772 16772
+ Partials 3739 3737 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why: The streamable HTTP proxy accepted any
MCP-Protocol-Versionheader —isSupportedMCPVersionunconditionally returnedtrue(pkg/transport/proxy/streamable/utils.go). This permissive stance is defensible for a transport-level proxy that doesn't depend on a specific MCP revision, but it was undocumented, and the spec says a server MUST reject an invalid/unsupported version with HTTP 400 — operators had no way to opt into that.What:
--strict-protocol-validationflag (default off, preserving today's version-agnostic behavior byte-for-byte via ap.strictProtocolValidation && …short-circuit).handlePostrejects a request whoseMCP-Protocol-Versionheader names an unknown MCP revision with HTTP 400; an absent header is still accepted (the streamable HTTP spec says to assume2025-03-26).{2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, mcp.MCPVersionModern}— the upcoming stateless revision is sourced frommcp.MCPVersionModern(not a duplicated literal) so the strict gate and theClassifyRevisionrouting path can't drift.TrustProxyHeaders: CLI (run_flags.go) →RunConfig→types.Config→StdioTransport→ streamable proxyOption; preserved acrossthv upgradevia the config applier.docs/arch/03-transport-architecture.md.Closes #5764 (items 1 & 2 landed in #5940 and #5944; this completes item 3).
Type of change
Test plan
go test -raceon the affected packages passes:TestHandlePost_StrictProtocolValidation(realhandlePostvia httptest): strict+unsupported→400, strict+supported→not-400, strict+absent→not-400, strict-off+unsupported→not-400 (default-behavior regression guard).TestIsSupportedMCPVersion: all five known revisions → true; unknown/garbage/empty → false.TestWithStrictProtocolValidation(runner builder) and thethv upgradeconfig-preservation guard extended to cover the new field (applier_test.go).task lint0 issues;task buildsucceeds;task docsregenerated (flag appears indocs/cli/thv_run.md).Does this introduce a user-facing change?
Yes. New
thv run --strict-protocol-validationflag (default off). When enabled, the streamable-HTTP proxy returns HTTP 400 for a request carrying an unknown/unsupportedMCP-Protocol-Versionheader; default behavior is unchanged (any version accepted).Special notes for reviewers
2026-07-28is required for consistency withClassifyRevision; 400-on-present-unsupported and accept-on-absent match the spec. Security: fail-closed, default-off preserves posture exactly, no bypass/race/DoS. Correctness: caught a wiring gap wherethv upgradedropped the persisted flag — fixed inapplier.go+ regression test.TrustProxyHeadersalso lives — are a deliberate follow-up, not included here.ClassifyRevisionpath uses a JSON-RPC error body. Both are spec-legal; unifying them is a possible follow-up.TestStandaloneSSE_ListChangedRefiltersThroughExistingMiddlewarefails onmainindependently of this PR (reproduced with these changes stashed) — not introduced here.Generated with Claude Code