Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions mcp/streamable_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@ type headerSchemaProperty struct {
Properties map[string]headerSchemaProperty `json:"properties,omitempty"`
}

// UnmarshalJSON decodes a single schema property without letting a sibling's
// unusual schema shape fail the whole schema. JSON Schema allows "type" to be
// an array (jsonschema.For emits ["null", "string"] for pointer fields) and a
// property to be a boolean subschema; neither carries an x-mcp-header that
// this code can use, so they are decoded leniently. A non-string "type" is
// kept as its raw JSON text, so it fails the primitive-type check.
func (p *headerSchemaProperty) UnmarshalJSON(data []byte) error {
var obj struct {
Type json.RawMessage `json:"type,omitempty"`
XMCPHeader json.RawMessage `json:"x-mcp-header,omitempty"`
Properties map[string]headerSchemaProperty `json:"properties,omitempty"`
}
if err := json.Unmarshal(data, &obj); err != nil {
var boolSchema bool
if json.Unmarshal(data, &boolSchema) == nil {
*p = headerSchemaProperty{}
return nil
}
return err
}
*p = headerSchemaProperty{XMCPHeader: obj.XMCPHeader, Properties: obj.Properties}
if len(obj.Type) > 0 {
var typeName string
if err := json.Unmarshal(obj.Type, &typeName); err != nil {
typeName = string(obj.Type)
}
p.Type = typeName
}
return nil
}

// unmarshalSchemaProperties normalizes any InputSchema type
// (*jsonschema.Schema, map[string]any, or json.RawMessage) into a common
// representation by marshaling to JSON and unmarshaling only the fields we need.
Expand Down
72 changes: 72 additions & 0 deletions mcp/streamable_headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,50 @@ func TestValidateToolParamHeaders(t *testing.T) {
wantErr bool
wantErrSub string
}{
{
name: "array type on an unannotated sibling does not hide an invalid annotation",
tool: &Tool{
Name: "test",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"count": map[string]any{"type": "number", "x-mcp-header": "Count"},
"note": map[string]any{"type": []any{"null", "string"}},
},
},
},
wantErr: true,
wantErrSub: "primitive types",
},
{
name: "nullable annotated property is rejected as non-primitive",
tool: &Tool{
Name: "test",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"note": map[string]any{"type": []any{"null", "string"}, "x-mcp-header": "Note"},
},
},
},
wantErr: true,
wantErrSub: "primitive types",
},
{
name: "boolean subschema does not hide an invalid annotation",
tool: &Tool{
Name: "test",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"count": map[string]any{"type": "number", "x-mcp-header": "Count"},
"extra": true,
},
},
},
wantErr: true,
wantErrSub: "primitive types",
},
{
name: "valid tool with x-mcp-header",
tool: &Tool{
Expand Down Expand Up @@ -1317,6 +1361,34 @@ func TestExtractToolParamHeaders(t *testing.T) {
},
want: map[string]string{"user.id": "UserId"},
},
{
name: "array type on an unannotated sibling does not hide annotations",
tool: &Tool{
Name: "test",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"region": map[string]any{"type": "string", "x-mcp-header": "Region"},
"note": map[string]any{"type": []any{"null", "string"}},
},
},
},
want: map[string]string{"region": "Region"},
},
{
name: "boolean subschema does not hide annotations",
tool: &Tool{
Name: "test",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"region": map[string]any{"type": "string", "x-mcp-header": "Region"},
"extra": true,
},
},
},
want: map[string]string{"region": "Region"},
},
}

for _, tt := range tests {
Expand Down
Loading