Skip to content

mcp: keep x-mcp-header handling working next to type arrays - #1410

Closed
RafalOfficial wants to merge 1 commit into
modelcontextprotocol:mainfrom
RafalOfficial:fix/param-header-type-arrays
Closed

RafalOfficial wants to merge 1 commit into
modelcontextprotocol:mainfrom
RafalOfficial:fix/param-header-type-arrays

Conversation

@RafalOfficial

Copy link
Copy Markdown

headerSchemaProperty decoded type into a string. One property with an array type (such as the ["null", "string"] that jsonschema.For infers for a pointer field) or a boolean subschema made decoding of the whole inputSchema fail. The client then sent no Mcp-Param-* headers for that tool, the server skipped validating them, and validateParamHeaderAnnotations accepted invalid annotations. Details and an end-to-end repro are in #1406.

This change keeps type as raw JSON and treats boolean subschemas as empty properties, so each property decodes on its own. An annotated property must still have a single primitive type; a nullable one (["null", "string"]) gets the existing "primitive types" error.

Tests: 5 new table cases in TestExtractToolParamHeaders and TestValidateToolParamHeaders, including one built with jsonschema.For and a pointer field. They fail on main and pass with this change. On Windows with Go 1.27, rebased on 3e9d812:

go build ./... && go vet ./... && gofmt -l .
go test ./...
go run honnef.co/go/tools/cmd/staticcheck@latest ./...   # 2026.2.2; CI's v0.6.1 can't read Go 1.27 export data

All pass and are clean.

Fixes #1406

This PR was written with Claude Code (AI-assisted).

🤖 Generated with Claude Code

headerSchemaProperty decoded "type" into a string, so one property
whose type is an array, such as the ["null", "string"] that
jsonschema.For infers for a pointer field, failed decoding of the whole
inputSchema. A boolean subschema did the same. The client then sent no
Mcp-Param-* headers for the tool, the server skipped validating them,
and validateParamHeaderAnnotations returned nil without checking any
annotation.

Keep "type" raw and accept boolean subschemas, so each property is
decoded on its own. An annotated property must still have a single
primitive type.

Fixes modelcontextprotocol#1406

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RafalOfficial

Copy link
Copy Markdown
Author

Heads-up for reviewers: #1408 fixes the same issue (#1406). Pick whichever you prefer; if it's #1408, I'll close this one.

One difference I checked: #1408's UnmarshalJSON decodes with encoding/json, so the keys inside a property schema match case-insensitively. For a property {"TYPE": "string", "X-MCP-Header": "Region"}, extractParamHeaderAnnotations returns a Region binding on #1408 (db40f4d), and none on main (25f4880) or on this PR. The rest of streamable_headers.go decodes with internaljson, which has been case-sensitive since #807.

(Checked with a throwaway test, written with Claude Code.)

@chrikrah chrikrah 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.

@RafalOfficial nothing in this diff blocks. At 2807cef the client sends Mcp-Param-Region: eu for the issue's with_optional tool again, and the five new cases fail with streamable_headers.go from the merge base 3e9d812.

$ go test -count=1 -run TestProbe1410 -v ./mcp/     # probe annotates "note" (*string) with x-mcp-header
# 3e9d812
ptr annotated   err=<nil> bindings=0
# 2807cef
ptr annotated   err=property "note": x-mcp-header can only be applied to primitive types (integer, string, boolean), got ["null","string"] bindings=1
$ go test -count=1 ./...     # 2807cef
ok  github.com/modelcontextprotocol/go-sdk/mcp  33.052s
$ go test -count=1 ./mcp/ -run 'TestExtractToolParamHeaders|TestValidateToolParamHeaders'     # tests kept, streamable_headers.go from 3e9d812
5 subtests FAIL
# not run: Windows, and the issue's HTTP program (I called generateParamHeaders on its schema)

Go 1.27.1, vet and gofmt clean. Also probed: ["string"] and ["string","null"] are rejected too, a false subschema is skipped, and a nested ["null","integer"] no longer hides its sibling.

non-blocking: rejecting ["null","string"] is the design you proposed in #1406, and docs/protocol.md says annotated properties must be string, integer or boolean. Neither text says where that lands. jsonschema.For types every *string that way, so an annotated pointer field now panics in AddTool. #1408 rejects the same union, so this applies to both pull requests.

Allowing ["null", <primitive>] in the doc and the check is the option #1406 offered, and it fits: a pointer is how Go marks an optional argument, and both header paths already skip a null one. @guglielmo-san, does that go in here, or is the panic the intended result?

@RafalOfficial

Copy link
Copy Markdown
Author

@chrikrah thanks for the probe. I reproduced it with a jsonschema.For struct whose *string field is annotated: at 2807cef, AddTool panics and the client's tools/list filter drops the tool. With streamable_headers.go from 3e9d812, the annotation is ignored silently (no error, no header).

The spec's constraint only lists integer, string and boolean. Its client/server table also says a null value means the header is omitted, so accepting ["null", <primitive>] fits. The change is small: read type as a string or an array, drop "null", and require exactly one primitive to remain. Add a test case and a line in docs/protocol.md.

@guglielmo-san if you want that, I'll add it to this PR. Otherwise the strict check stays as is. The same choice applies to #1408.

@guglielmo-san

Copy link
Copy Markdown
Contributor

See comment #1406 (comment)

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.

mcp: a property with an array "type" (any pointer field from jsonschema.For) disables x-mcp-header handling for the whole tool

3 participants