Repository navigation
Description
Describe the bug
headerSchemaProperty (mcp/streamable_headers.go) decodes type into a string. JSON Schema also allows an array there, and jsonschema.For emits one for every pointer field ("type": ["null", "string"]). When any property in a tool's inputSchema has an array type, the remarshal in unmarshalSchemaProperties fails and returns nil. The client and server then treat the tool as having no x-mcp-header annotations at all, even on other properties:
- The client sends no
Mcp-Param-*headers for the tool. A server that validates them, as the 2026-07-28 spec requires, answers withHeaderMismatch. - The server skips
validateParamHeaders, so a request whoseMcp-Param-*header contradicts the body is accepted. validateParamHeaderAnnotationsreturns nil without checking anything, soAddTooland the client'stools/listfilter also accept an invalid annotation, such as one on anumberproperty.
A boolean subschema ("properties": {"extra": true}) fails the same way.
To Reproduce
A typed tool with an annotated region and an optional *string field, served over Streamable HTTP and called by the go-sdk client on 2026-07-28 (go run . with a replace to the checkout):
type withOptionalIn struct {
Region string `json:"region"`
Query string `json:"query"`
Note *string `json:"note,omitempty"` // inferred as "type": ["null", "string"]
}
s, _ := jsonschema.For[withOptionalIn](nil)
s.Properties["region"].Extra = map[string]any{"x-mcp-header": "Region"}
mcp.AddTool(server, &mcp.Tool{Name: "with_optional", InputSchema: s}, handler)The full program registers this tool next to an identical plain tool without Note. The program serves both tools with NewStreamableHTTPHandler (stateless, TTLMs set so the client caches tools/list). It calls each tool with {"region": "eu", "query": "q"} and records the outgoing headers in a RoundTripper. Then it calls each tool again with Mcp-Param-Region overwritten to forged:
plain client sent Mcp-Param-Region="eu", call err=<nil>
with_optional client sent Mcp-Param-Region="", call err=<nil>
plain header forged to "forged" (body region=eu): call err=... header mismatch: Mcp-Param-Region header value 'forged' does not match body value: rejected by transport: Bad Request
with_optional header forged to "forged" (body region=eu): call err=<nil>
The only difference between the two tools is the extra optional field.
Expected behavior
with_optional behaves like plain: the client sends Mcp-Param-Region: eu, and the server rejects the forged header.
Proposed fix
In headerSchemaProperty, keep type as a json.RawMessage. Give the struct an UnmarshalJSON that turns true/false into an empty property, so each property decodes independently. An annotated property must still have a single primitive type. A nullable annotated property (["null", "string"]) is rejected with the existing "primitive types" error, like the TypeScript SDK's check (typeof schema.type === 'string'). The change is about 20 lines, plus table cases in TestExtractToolParamHeaders and TestValidateToolParamHeaders. If you'd rather accept ["null", <primitive>] on annotated properties (both header paths already skip null arguments), that is a small change.
Patch: main...RafalOfficial:go-sdk:fix/param-header-type-arrays
The new cases fail on main and pass with the patch. With the patch, the repro output matches for both tools. go test ./..., go vet ./..., gofmt -l . and staticcheck 2026.2.2 are clean. I couldn't run v0.6.1, the CI version, because it can't read Go 1.27 export data. This is separate from #1250 / #1341, which covers annotations in positions that are not reachable through properties.
If you're happy with this direction, I can open the PR.
Additional context
- Go MCP SDK:
mainat 9012a21 go version go1.27.0 windows/amd64
This report and the patch were written with Claude Code (AI-assisted). I ran the repro and the tests above on my machine.
Activity
guglielmo-san commentedon Oct 16, 2026
@RafalOfficial thank you for reporting the issue, feel free to open the PR!
mcp: keep x-mcp-header handling working next to type arrays
guglielmo-san commentedon Oct 27, 2026
Closing as fixed in #1459