Skip to content

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

Closed

Description

@RafalOfficial

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 with HeaderMismatch.
  • The server skips validateParamHeaders, so a request whose Mcp-Param-* header contradicts the body is accepted.
  • validateParamHeaderAnnotations returns nil without checking anything, so AddTool and the client's tools/list filter also accept an invalid annotation, such as one on a number property.

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: main at 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

  1. guglielmo-san commented on Oct 16, 2026

    @guglielmo-san
    Contributor

    @RafalOfficial thank you for reporting the issue, feel free to open the PR!

  2. added a commit that references this issue on Oct 17, 2026
    2807cef
  3. guglielmo-san commented on Oct 27, 2026

    @guglielmo-san
    Contributor

    Closing as fixed in #1459

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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