fix(authz): accept JSON-RPC responses to server-initiated requests - #6521
Open
jstar0 wants to merge 3 commits into
Open
fix(authz): accept JSON-RPC responses to server-initiated requests#6521jstar0 wants to merge 3 commits into
jstar0 wants to merge 3 commits into
Conversation
Issue stacklok#6497 reported a healthy Tableau MCP backend being marked unavailable because the vMCP health probe used a bare HTTP GET, which that backend rejects with 400 for lack of a session id. Current main no longer probes with GET: the health check is BackendClient.ListCapabilities, and newStreamableHTTPClient enables transport.WithContinuousListening — the standalone server->client SSE stream, the only GET vMCP makes — solely on the forwarding tools/call path. Nothing pinned that, so add the regression the issue describes: a real go-sdk stateful streamable-HTTP backend behind a handler that answers every GET with Tableau's 400, asserting that ListCapabilities both succeeds and issues no GET at all. Verified to have teeth: appending WithContinuousListening unconditionally in newStreamableHTTPClient makes the test fail on the GET assertion.
A client POSTing its answer to a server-initiated request — a ping result today, elicitation and sampling results as those land — got 400 "Invalid or malformed MCP request" whenever authorization was enabled. Clients treat that as a fatal transport error: VS Code tears the session down and retries with a new session id, so the connection flaps once per ping interval. parseMCPRequest handles only JSON-RPC requests and returns nil for a response, which the authz middleware could not tell apart from an unparseable body, so it took the malformed-body branch. Record in the parsing middleware that a body decoded as a JSON-RPC response or error, and let the authz middleware pass those through. A response names no method and reaches no tool, so there is nothing to authorize; the streamable-HTTP transport already answers 202 for it, as the spec requires. The content-type refusal ahead of this is untouched: a JSON-RPC body smuggled under text/plain is still rejected before parsing, and a genuinely malformed JSON body still gets the 400. Fixes stacklok#5009
jstar0
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
blkt,
jerm-dro,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
September 6, 2026 14:40
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6521 +/- ##
==========================================
- Coverage 78.43% 78.38% -0.06%
==========================================
Files 776 776
Lines 76094 76108 +14
==========================================
- Hits 59685 59656 -29
- Misses 16404 16447 +43
Partials 5 5 ☔ 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.
Fixes #5009.
@eleftherias — picking this up at last. The diagnosis moved since the issue was filed, so the fix is not where the issue pointed.
Where it actually is now
The issue named
pkg/transport/proxy/streamable/and expected a response-vs-request branch in the POST handler. That branch exists onmaintoday:handleNotificationOrClientResponsematchesisClientResponse(msg)and answers202 Accepted. The transport is spec-correct.But the reported error string —
Invalid or malformed MCP request— does not appear in the transport at all. It lives inpkg/authz/middleware.go, and the request never reaches the transport when authorization is enabled:parseMCPRequesthandles only*jsonrpc2.Requestand returnsnilfor a response ("Response or error messages are not parsed here").parsedRequest == niland cannot tell that apart from a body that failed to parse, so it takes the malformed-body branch and writes the 400.So the symptom survives exactly as reported, one layer earlier than expected. A failing test first, on unmodified
main:The change
ParsingMiddlewarenow records in the context when a body decoded as a JSON-RPC response or error, and the authz middleware passes those through instead of rejecting them. A response names no method and reaches no tool, so there is nothing for a policy to decide.I was careful to keep this narrow, because the branch it touches is load-bearing for security:
text/plainis still refused before parsing, andTestMiddlewareRejectsNonJSONPostandTestMiddlewareNonJSONPostVariantsstill pass unchanged.jsonrpc2.DecodeMessageto succeed and yield a*jsonrpc2.Response.ParsingMiddlewareand is unaffected.The new test covers both shapes a client can send back, a
resultand anerror.Verification
go test ./pkg/authz/... ./pkg/mcp/... ./pkg/audit/... ./pkg/transport/proxy/streamable/...all pass;golangci-lint run ./pkg/authz/... ./pkg/mcp/...reports 0 issues.One thing worth a maintainer's eye: this makes authz pass a response through without any policy evaluation. That is the only coherent behaviour I can see — there is no method, no tool, and no resource to authorize against, and the alternative is the session teardown in the issue — but if you'd rather gate it (say, only when a matching server-initiated request is pending for that session), that correlation state does not exist yet and would be a larger change. Happy to go that way if you prefer it.