Skip to content

Authz-denied responses emit non-conformant JSON-RPC (capitalized keys, no jsonrpc field) #5950

Description

@jhrozek

Summary

The authorization-denial path emits a response body that is not valid JSON-RPC 2.0: the top-level keys are Go-capitalized (Result/Error/ID) and the mandatory "jsonrpc":"2.0" field is absent. A spec-conformant (case-sensitive) MCP client cannot parse error/id, and the missing jsonrpc field is itself a violation.

Evidence (actual wire bytes)

POST /mcp with a server/discover call to an authz-enabled proxy (HTTP 403):

{"Result":null,"Error":{"code":403,"message":"unknown MCP method: server/discover (not configured for authorization)"},"ID":{}}

Root cause

pkg/authz/middleware.go handleUnauthorized (~L127-151) builds a *jsonrpc2.Response (golang.org/x/exp/jsonrpc2) and does json.NewEncoder(w).Encode(errorResponse) directly on the struct. In the pinned version of that package, Response{Result, Error, ID} has no json tags and no custom MarshalJSON, so encoding/json reflection produces capitalized keys and omits jsonrpc. Every other JSON-RPC response path in the codebase (pkg/mcp/classification_response.go, pkg/transport/session/jsonrpc_errors.go) instead hand-builds a map[string]any with lowercase keys and an explicit "jsonrpc":"2.0" — this denial path is the lone inconsistency.

Impact

  • Conformant clients (case-sensitive JSON parsers, including the MCP go-sdk) cannot read the denial's error/id.
  • Likely interacts with a Modern (2026-07-28) go-sdk client's behavior: on a server/discover 403 it appears to fall back / downgrade rather than surface the denial, plausibly because it cannot parse this malformed error as a JSON-RPC error. (Observed while building dual-era e2e tests; not root-caused to this specifically, flagging the interaction.)
  • ToolHive's own hand-rolled test client only tolerates it via encoding/json's case-insensitive unmarshal.

Suggested fix

Emit a conformant envelope on this path like the others do — hand-build the lowercase map[string]any with "jsonrpc":"2.0", "id", "error":{"code","message"} (or route through a shared conformant encoder), rather than json.Encode-ing the jsonrpc2.Response struct.

Context

Found while building dual-era stateless proxy e2e tests (#5837). Low-frequency path but client-facing and spec-relevant.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions