Refresh the cached MCP parse after webhook mutation - #6136
Merged
Conversation
Any middleware that rewrites the request body (e.g. the mutating webhook) leaves downstream authz/audit/telemetry deciding on the bytes that arrived rather than the bytes the backend will execute, since ParsingMiddleware only parses once and parseMCPRequest is package-private. This helper re-parses after a body rewrite and republishes the result into the context and ParsedRequestHolder, rejecting batch bodies via the existing BatchUnsupportedError before parsing.
ParsingMiddleware parses the request body once and publishes the result into the request context and into a ParsedRequestHolder, then refuses to parse again. The mutating webhook middleware replaced r.Body with the patched body but passed the request through unchanged, so authorization evaluated Cedar policy against the tool name and arguments that arrived while the backend executed the ones the webhook produced. Audit named a request that never ran, and it did so even at the default configuration because the event type and target name are not gated on includeRequestData. Telemetry and usage metrics drifted the same way. Republish the parse when the body actually changed, which corrects every consumer that reads it rather than patching each one. The re-parse runs through the same batch guard as ParsingMiddleware so a patched body can never smuggle a JSON-RPC batch past controls that inspect one request at a time, and an unparseable result fails closed instead of reaching the backend. Also update ContentLength alongside the body. A patch almost always changes the body length, and the reverse proxy rejects a forwarded request whose declared length disagrees with its readable body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jhrozek
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
blkt and
rdimitrov
as code owners
July 29, 2026 09:32
10 tasks
The middleware doc carried two ordering lists that disagreed: the one describing webhook configuration included mutating and validating webhooks, while the reference list omitted them entirely and jumped from the MCP parser straight to usage metrics. Reconcile them against the code and record which build path the reference list describes, since the CLI path has no rate limiting and orders usage metrics differently. State that a mutating webhook steers what authorization evaluates. That is the intended contract now that the parse is republished, and it deserves to be a written decision rather than an inference. Document two gaps republishing does not close: the Mcp-Method and Mcp-Name headers forwarded to a backend still describe the pre-rename tool, and the tool-call filter and rate limiter run outside the parser so they still decide against the request as received. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jhrozek
force-pushed
the
fix-mutation-parse-staleness
branch
from
July 29, 2026 09:34
98ce02e to
9a25e53
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6136 +/- ##
==========================================
- Coverage 72.56% 72.51% -0.05%
==========================================
Files 734 734
Lines 75912 75939 +27
==========================================
- Hits 55084 55067 -17
- Misses 16920 16980 +60
+ Partials 3908 3892 -16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
approved these changes
Jul 29, 2026
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
pre-mutation MCP request while the backend executed the post-mutation body.
ParsingMiddlewareparses the body once and publishes the parse into the request context andinto a
ParsedRequestHolder, then refuses to parse again. The mutating webhook replacedr.Bodybut passed the request through unchanged, so Cedar evaluated policy against the toolname and arguments that arrived, not the ones that ran.
docs/middleware.md:31documented theopposite behavior.
target.namearenot gated on
includeRequestData, so the audit trail named a request that never executed.Telemetry and usage metrics drifted the same way.
every consumer that reads the cached parse, rather than patching four of them. The re-parse
routes through the same
IsBatchRequestguard asParsingMiddleware, so a patched body stillcannot smuggle a JSON-RPC batch past controls that inspect one request per call. An unparseable
result fails closed instead of reaching the backend.
r.ContentLengthalongsider.Body. A patch almost always changes the bodylength, and the reverse proxy rejects a forwarded request whose declared length disagrees with
its readable body. This was a pre-existing bug at the same lines.
Fixes #6133
Type of change
Test plan
task test)task test-e2e)task lint-fix)Unit tests were run per-package with the Taskfile's flags
(
go test -ldflags=-extldflags=-Wl,-w -race) rather than as a fulltask testsweep, acrosspkg/mcp,pkg/webhook/...,pkg/authz/...,pkg/audit,pkg/runner/...,pkg/telemetry/...,pkg/usagemetricsandpkg/ratelimit/...— 18 packages green from a cleared test cache, plusgo build ./....task lint-fixhas not been run; relying on CI for that.The three regression tests were written first and confirmed failing against unmodified code, each
on a value-mismatch assertion showing the pre-mutation value (
read_file,SELECT *), not on acompile error or panic.
Changes
pkg/mcp/parser.goRepublishParsedMCPRequest: batch-guards, re-parses, re-attaches the Modern header fields, refreshes both the context value and the holder.pkg/mcp/parser_test.gopkg/webhook/mutating/middleware.goContentLength; batch errors reuseWriteBatchUnsupportedError, others reusesendErrorResponse.pkg/webhook/mutating/middleware_test.gopkg/runner/config_builder.godocs/middleware.mdDoes this introduce a user-facing change?
Yes. With a mutating webhook and authorization both configured, Cedar policy is now evaluated
against the mutated request rather than the request as received, and audit events, spans and
metrics name the tool that actually executed. Operators whose policies were written against
un-normalized values — and were passing only because policy never saw the webhook's rewrite —
may see decisions change. That is the documented contract (
docs/middleware.md:31) taking effect.Special notes for reviewers
Why
bytes.Equalgates the republish. Only republishing when the body changed bounds thechange to the mutation case. Without it, every no-patch and fail-open path would newly re-parse,
and five existing fail-open tests pass a body of
{"jsonrpc":"2.0","id":1}that has nomethodand so is not a parseable JSON-RPC request — they would start returning 500. The guard is the
correct answer rather than a workaround: in production an unmutated body provably parsed already,
because this middleware returns early when no parse exists.
Two gaps this deliberately does not close, both documented rather than fixed:
Mcp-Method/Mcp-Nameheaders forwarded to the backendstill describe the original name, since a webhook patches only the JSON body.
ValidateHeaderConsistencyis enforced only in vMCP today, which never wires this middleware,so nothing here catches it — but a conformant Modern backend rejects the mismatch, so it fails
closed. Re-rendering headers means fabricating client-supplied values with sentinel encoding,
which is a separate decision.
ParsingMiddlewareand so still decideagainst the request as received. Republishing cannot reach them. Filed separately as Tool filtering and rate limiting are enforced against the pre-mutation request #6134.
Coupling with #5800. That PR moves telemetry from after the webhook layers to immediately
after the parser, which would put telemetry outside the mutation and make it read the pre-mutation
parse again — it has no
ParsedRequestHolder, and a derived context only flows downstream, sorepublishing cannot reach it there. Whichever lands second needs to reconcile. If this merges
first, #5800 should update the ordering list and limitations bullet in
docs/middleware.md, theinline comment at the republish site, and the
RepublishParsedMCPRequestdoc comment, which namesthe rate limiter as an uncorrected consumer. I have commented on #5800 with the details. There is
no textual conflict in either direction.
Latent, not fixed here. This makes the holder a fresher copy than the context value, where
previously both were equally stale.
pkg/audit/auditor.goreads context-then-holder. Audit isoutermost in every wired chain, so its own context never holds a parse and it always falls through
to the fresh holder — the precedence would only matter if the parser ran outside audit while a
mutating webhook ran inside, which no current chain does. Left alone as out of scope.
Generated with Claude Code