mcp: a property with an array "type" (any pointer field from jsonschema.For) disables x-mcp-header handling for the whole tool
Maintainers usually reply within 1 day
@jay23606 is already working on this.
Since Oct 16, 2026.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 50/100
Research direction
Start in mcp/streamable_headers.go: headerSchemaProperty decodes type as a string, so an array or boolean subschema makes unmarshalSchemaProperties return nil for the whole tool. Read validateParamHeaderAnnotations and the two tests named in the issue, TestExtractToolParamHeaders and TestValidateToolParamHeaders. Done when a property with an array or boolean schema no longer disables annotations on other properties, and an annotated nullable property is rejected with the existing primitive-types error, as the issue proposes. The maintainers still need to decide the nullable case.
Written by the indexing model from the issue text.
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.
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.
- Dominant language
- Go
- Stars
- 5.2k
- Forks
- 583
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 55
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/go-sdk
-
Session-lifecycle bookkeeping logs at info, spamming multiple log lines per request on stateless streamable HTTPPossibly taken @anneheartrecord claimed this 51 days ago. OpenP3
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
modelcontextprotocol/go-sdk#1204 · 1 reaction ·
Maintainers usually reply within 1 day
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensionsPossibly taken @ajuijas claimed this 217 days ago. Openneeds investigation
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
modelcontextprotocol/go-sdk#745 · 13 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
jsonrpc: decimal and exponent integer IDs still lose precision after #1401Possibly taken @skosovsky claimed this 2 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 25/100
modelcontextprotocol/go-sdk#1404 · 2 comments ·
Maintainers usually reply within 1 day
-
mcp: rejected request IDs receive no JSON-RPC error envelopePossibly taken @skosovsky claimed this 6 days ago. Open
Difficulty 4/5 1-2 days Newbie friendliness 68/100
modelcontextprotocol/go-sdk#1391 ·
Maintainers usually reply within 1 day
-
auth: Authorize fails against a 2025-03-26 server whose URL has a query stringPossibly taken @nahapetyan-serob claimed this 4 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 40/100
modelcontextprotocol/go-sdk#1347 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in modelcontextprotocol/go-sdk
Similar issues
-
Chained slog With calls discard previously bound attributesPossibly taken @yuefdev claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
[scanner] TestAgentNowLineStates hard-fails instead of degrading when node is off PATHPossibly taken @arteeh claimed this today. Openagent/scanner ci help wanted kind/test
Difficulty 1/5 Under an hour Newbie friendliness 90/100
hivecommons/hive#12924 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Gentleman-Programming/engram#1940 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
cloudflare/terraform-provider-cloudflare#7423 ·
Maintainers usually reply within 1 day
-
devices
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
evcc-io/evcc#35007 · 7 comments ·
Maintainers usually reply within 1 day