From b54fb1467d80f0fe736b883ba56096995f20d758 Mon Sep 17 00:00:00 2001 From: jesse-engineer <784909593@qq.com> Date: Thu, 16 Apr 2026 12:02:07 +0800 Subject: [PATCH 1/3] fix(protocol): support JSON Schema type as string or array (union) Property.Type is now PropertyType ([]DataType) with custom JSON marshal/unmarshal so remote tool schemas like type: ["string","number"] decode correctly (e.g. MCP ListTools). Validation treats multiple types as a union. Adds regression tests for unmarshaling and union validation. Fixes ThinkInAIXYZ/go-mcp#198 Made-with: Cursor --- client/client_test.go | 2 +- protocol/schema_generate.go | 65 +++++++++-- protocol/schema_generate_test.go | 59 +++++----- protocol/schema_validate.go | 20 +++- protocol/schema_validate_test.go | 184 ++++++++++++++++++------------- 5 files changed, 211 insertions(+), 119 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 49c64db..7ae52b0 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -132,7 +132,7 @@ func TestClientCall(t *testing.T) { Type: protocol.Object, Properties: map[string]*protocol.Property{ "timezone": { - Type: "string", + Type: protocol.PropertyType{protocol.String}, Description: "current time timezone", }, }, diff --git a/protocol/schema_generate.go b/protocol/schema_generate.go index 2f4787c..865c02d 100644 --- a/protocol/schema_generate.go +++ b/protocol/schema_generate.go @@ -1,6 +1,7 @@ package protocol import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -21,8 +22,54 @@ const ( Boolean DataType = "boolean" ) +// PropertyType is a JSON Schema "type" keyword: either a single type string or a union (array of strings). +type PropertyType []DataType + +func (pt PropertyType) MarshalJSON() ([]byte, error) { + if len(pt) == 0 { + return json.Marshal("") + } + if len(pt) == 1 { + return json.Marshal(string(pt[0])) + } + ss := make([]string, len(pt)) + for i, t := range pt { + ss[i] = string(t) + } + return json.Marshal(ss) +} + +func (pt *PropertyType) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + *pt = nil + return nil + } + if string(data) == "null" { + *pt = nil + return nil + } + var s string + if err := json.Unmarshal(data, &s); err == nil { + if s == "" { + *pt = nil + return nil + } + *pt = PropertyType{DataType(s)} + return nil + } + var arr []string + if err := json.Unmarshal(data, &arr); err != nil { + return err + } + *pt = make(PropertyType, len(arr)) + for i, x := range arr { + (*pt)[i] = DataType(x) + } + return nil +} + type Property struct { - Type DataType `json:"type"` + Type PropertyType `json:"type"` // Description is the description of the schema. Description string `json:"description,omitempty"` // Items specifies which data type an array contains, if the schema type is Array. @@ -145,7 +192,7 @@ func reflectSchemaByObject(t reflect.Type) (*Property, error) { } property := &Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: properties, Required: requiredFields, Enum: enumValues, @@ -158,16 +205,16 @@ func reflectSchemaByType(t reflect.Type) (*Property, error) { switch t.Kind() { case reflect.String: - s.Type = String + s.Type = PropertyType{String} case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - s.Type = Integer + s.Type = PropertyType{Integer} case reflect.Float32, reflect.Float64: - s.Type = Number + s.Type = PropertyType{Number} case reflect.Bool: - s.Type = Boolean + s.Type = PropertyType{Boolean} case reflect.Slice, reflect.Array: - s.Type = Array + s.Type = PropertyType{Array} items, err := reflectSchemaByType(t.Elem()) if err != nil { return nil, err @@ -178,14 +225,14 @@ func reflectSchemaByType(t reflect.Type) (*Property, error) { if err != nil { return nil, err } - object.Type = ObjectT + object.Type = PropertyType{ObjectT} s = object case reflect.Map: if t.Key().Kind() != reflect.String { return nil, fmt.Errorf("map key type %s is not supported", t.Key().Kind()) } object := &Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, } s = object case reflect.Ptr: diff --git a/protocol/schema_generate_test.go b/protocol/schema_generate_test.go index 3baaa72..5581f62 100644 --- a/protocol/schema_generate_test.go +++ b/protocol/schema_generate_test.go @@ -1,6 +1,7 @@ package protocol import ( + "slices" "sort" "testing" ) @@ -59,26 +60,26 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string": { - Type: String, + Type: PropertyType{String}, Description: "string", }, "number": { - Type: Number, + Type: PropertyType{Number}, }, "string4enum": { - Type: String, + Type: PropertyType{String}, Enum: []string{"a", "b", "c"}, }, "integer4enum": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}, }, "number4enum": { - Type: Number, + Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}, }, "number4enum2": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}, }, }, @@ -94,26 +95,26 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string": { - Type: String, + Type: PropertyType{String}, Description: "string", }, "number": { - Type: Number, + Type: PropertyType{Number}, }, "string4enum": { - Type: String, + Type: PropertyType{String}, Enum: []string{"a", "b", "c"}, }, "integer4enum": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}, }, "number4enum": { - Type: Number, + Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}, }, "number4enum2": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}, }, }, @@ -168,20 +169,20 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, Description: "user name", }, "age": { - Type: Integer, + Type: PropertyType{Integer}, }, "address": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "city": { - Type: String, + Type: PropertyType{String}, }, "street": { - Type: String, + Type: PropertyType{String}, }, }, Required: []string{"city"}, @@ -203,13 +204,13 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "id": { - Type: Integer, + Type: PropertyType{Integer}, }, "email": { - Type: String, + Type: PropertyType{String}, }, "active": { - Type: Boolean, + Type: PropertyType{Boolean}, }, }, Required: []string{"id", "active"}, @@ -232,19 +233,19 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, }, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "age": { - Type: Integer, + Type: PropertyType{Integer}, }, "active": { - Type: Boolean, + Type: PropertyType{Boolean}, }, }, Required: []string{"active"}, @@ -270,13 +271,13 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, }, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, }, }, Required: []string{"name", "info"}, @@ -353,7 +354,7 @@ func compareProperty(a, b *Property) bool { if a == nil || b == nil { return false } - if a.Type != b.Type { + if !slices.Equal(a.Type, b.Type) { return false } if a.Description != b.Description { diff --git a/protocol/schema_validate.go b/protocol/schema_validate.go index 21f6521..c37e2e4 100644 --- a/protocol/schema_validate.go +++ b/protocol/schema_validate.go @@ -30,7 +30,7 @@ func VerifyAndUnmarshal(content json.RawMessage, v any) error { } return verifySchemaAndUnmarshal(Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: schema.Properties, Required: schema.Required, }, content, v) @@ -49,7 +49,23 @@ func verifySchemaAndUnmarshal(schema Property, content []byte, v any) error { } func validate(schema Property, data any) bool { - switch schema.Type { + types := schema.Type + if len(types) == 0 { + return false + } + if len(types) == 1 { + return validateWithType(types[0], schema, data) + } + for _, typ := range types { + if validateWithType(typ, schema, data) { + return true + } + } + return false +} + +func validateWithType(typ DataType, schema Property, data any) bool { + switch typ { case ObjectT: return validateObject(schema, data) case Array: diff --git a/protocol/schema_validate_test.go b/protocol/schema_validate_test.go index 258b038..21764e8 100644 --- a/protocol/schema_validate_test.go +++ b/protocol/schema_validate_test.go @@ -17,83 +17,83 @@ func Test_Validate(t *testing.T) { want bool }{ // string integer number boolean - {"", args{data: "ABC", schema: Property{Type: String}}, true}, - {"", args{data: 123, schema: Property{Type: String}}, false}, - {"", args{data: "a", schema: Property{Type: String, Enum: []string{"a", "b", "c"}}}, true}, - {"", args{data: "d", schema: Property{Type: String, Enum: []string{"a", "b", "c"}}}, false}, - {"", args{data: 123, schema: Property{Type: Integer}}, true}, - {"", args{data: 123.4, schema: Property{Type: Integer}}, false}, - {"", args{data: 1, schema: Property{Type: Integer, Enum: []string{"1", "2", "3"}}}, true}, - {"", args{data: 4, schema: Property{Type: Integer, Enum: []string{"1", "2", "3"}}}, false}, - {"", args{data: "ABC", schema: Property{Type: Number}}, false}, - {"", args{data: 123, schema: Property{Type: Number}}, true}, - {"", args{data: 1.1, schema: Property{Type: Number, Enum: []string{"1.1", "2.2", "3.3"}}}, true}, - {"", args{data: 4.4, schema: Property{Type: Number, Enum: []string{"1.1", "2.2", "3.3"}}}, false}, - {"", args{data: 1, schema: Property{Type: Number, Enum: []string{"1", "2", "3"}}}, true}, - {"", args{data: 4, schema: Property{Type: Number, Enum: []string{"1", "2", "3"}}}, false}, - {"", args{data: false, schema: Property{Type: Boolean}}, true}, - {"", args{data: 123, schema: Property{Type: Boolean}}, false}, - {"", args{data: nil, schema: Property{Type: Null}}, true}, - {"", args{data: 0, schema: Property{Type: Null}}, false}, + {"", args{data: "ABC", schema: Property{Type: PropertyType{String}}}, true}, + {"", args{data: 123, schema: Property{Type: PropertyType{String}}}, false}, + {"", args{data: "a", schema: Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}}, true}, + {"", args{data: "d", schema: Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}}, false}, + {"", args{data: 123, schema: Property{Type: PropertyType{Integer}}}, true}, + {"", args{data: 123.4, schema: Property{Type: PropertyType{Integer}}}, false}, + {"", args{data: 1, schema: Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}}, true}, + {"", args{data: 4, schema: Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}}, false}, + {"", args{data: "ABC", schema: Property{Type: PropertyType{Number}}}, false}, + {"", args{data: 123, schema: Property{Type: PropertyType{Number}}}, true}, + {"", args{data: 1.1, schema: Property{Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}}}, true}, + {"", args{data: 4.4, schema: Property{Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}}}, false}, + {"", args{data: 1, schema: Property{Type: PropertyType{Number}, Enum: []string{"1", "2", "3"}}}, true}, + {"", args{data: 4, schema: Property{Type: PropertyType{Number}, Enum: []string{"1", "2", "3"}}}, false}, + {"", args{data: false, schema: Property{Type: PropertyType{Boolean}}}, true}, + {"", args{data: 123, schema: Property{Type: PropertyType{Boolean}}}, false}, + {"", args{data: nil, schema: Property{Type: PropertyType{Null}}}, true}, + {"", args{data: 0, schema: Property{Type: PropertyType{Null}}}, false}, // array {"", args{ data: []any{"a", "b", "c"}, schema: Property{ - Type: Array, Items: &Property{Type: String}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}}, }, }, true}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: String}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}}, }, }, false}, {"", args{ data: []any{"a"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []string{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, }, }, true}, {"", args{ data: []any{"a", "b", "c"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []string{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, }, }, true}, {"", args{ data: []any{"d"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []string{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, }, }, false}, {"", args{ data: []any{"a", "b", "c", "d"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []string{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, }, }, false}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: Integer}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}}, }, }, true}, {"", args{ data: []any{1, 2, 3.4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}}, }, }, false}, {"", args{ data: []any{1}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, }, }, true}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, }, }, true}, {"", args{ data: []any{1, 2, 3, 4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, }, }, false}, {"", args{ data: []any{4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, }, }, false}, // object @@ -104,12 +104,12 @@ func Test_Validate(t *testing.T) { "boolean": false, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, - "number": {Type: Number}, - "boolean": {Type: Boolean}, - "array": {Type: Array, Items: &Property{Type: Number}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, + "number": {Type: PropertyType{Number}}, + "boolean": {Type: PropertyType{Boolean}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}}, }, Required: []string{"string"}, }}, true}, @@ -119,12 +119,12 @@ func Test_Validate(t *testing.T) { "boolean": false, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, - "number": {Type: Number}, - "boolean": {Type: Boolean}, - "array": {Type: Array, Items: &Property{Type: Number}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, + "number": {Type: PropertyType{Number}}, + "boolean": {Type: PropertyType{Boolean}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}}, }, Required: []string{"string"}, }}, false}, @@ -135,12 +135,12 @@ func Test_Validate(t *testing.T) { "number4Int": 1, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String, Enum: []string{"a", "b", "c"}}, - "integer": {Type: Integer, Enum: []string{"1", "2", "3"}}, - "number": {Type: Number, Enum: []string{"1.1", "2.2", "3.3"}}, - "number4Int": {Type: Number, Enum: []string{"1", "2", "3"}}, - "array": {Type: Array, Items: &Property{Type: Number}, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, + "integer": {Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, + "number": {Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}}, + "number4Int": {Type: PropertyType{Number}, Enum: []string{"1", "2", "3"}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}, Enum: []string{"1", "2", "3"}}, }, Required: []string{"string"}, }}, true}, @@ -151,12 +151,12 @@ func Test_Validate(t *testing.T) { "number4Int": 4, "array": []any{4}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String, Enum: []string{"a", "b", "c"}}, - "integer": {Type: Integer, Enum: []string{"1", "2", "3"}}, - "number": {Type: Number, Enum: []string{"1.1", "2.2", "3.3"}}, - "number4Int": {Type: Number, Enum: []string{"1", "2", "3"}}, - "array": {Type: Array, Items: &Property{Type: Number}, Enum: []string{"1", "2", "3"}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}, Enum: []string{"a", "b", "c"}}, + "integer": {Type: PropertyType{Integer}, Enum: []string{"1", "2", "3"}}, + "number": {Type: PropertyType{Number}, Enum: []string{"1.1", "2.2", "3.3"}}, + "number4Int": {Type: PropertyType{Number}, Enum: []string{"1", "2", "3"}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}, Enum: []string{"1", "2", "3"}}, }, Required: []string{"string"}, }}, false}, @@ -169,16 +169,16 @@ func Test_Validate(t *testing.T) { }, }, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "name": {Type: String}, + "name": {Type: PropertyType{String}}, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "age": {Type: Integer}, - "active": {Type: Boolean}, + "age": {Type: PropertyType{Integer}}, + "active": {Type: PropertyType{Boolean}}, }, Required: []string{"active"}, }, @@ -197,16 +197,16 @@ func Test_Validate(t *testing.T) { }, }, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "name": {Type: String}, + "name": {Type: PropertyType{String}}, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "age": {Type: Integer}, - "active": {Type: Boolean}, + "age": {Type: PropertyType{Integer}}, + "active": {Type: PropertyType{Boolean}}, }, Required: []string{"active"}, }, @@ -239,10 +239,10 @@ func TestUnmarshal(t *testing.T) { }{ {"", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "number": {Type: Number}, + "string": {Type: PropertyType{String}}, + "number": {Type: PropertyType{Number}}, }, }, content: []byte(`{"string":"abc","number":123.4}`), @@ -253,10 +253,10 @@ func TestUnmarshal(t *testing.T) { }, false}, {"", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "number": {Type: Number}, + "string": {Type: PropertyType{String}}, + "number": {Type: PropertyType{Number}}, }, Required: []string{"string", "number"}, }, @@ -268,10 +268,10 @@ func TestUnmarshal(t *testing.T) { }, true}, {"validate integer", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, }, Required: []string{"string", "integer"}, }, @@ -283,10 +283,10 @@ func TestUnmarshal(t *testing.T) { }, false}, {"validate integer failed", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, }, Required: []string{"string", "integer"}, }, @@ -462,3 +462,31 @@ func TestVerifyAndUnmarshal(t *testing.T) { }) } } + +func TestInputSchema_JSON_unionType(t *testing.T) { + const payload = `{"type":"object","properties":{"x":{"type":["string","number"]}}}` + var schema InputSchema + if err := json.Unmarshal([]byte(payload), &schema); err != nil { + t.Fatal(err) + } + x, ok := schema.Properties["x"] + if !ok || x == nil { + t.Fatal("missing x") + } + if len(x.Type) != 2 || x.Type[0] != String || x.Type[1] != Number { + t.Fatalf("Type = %#v", x.Type) + } +} + +func Test_validate_unionType(t *testing.T) { + schema := Property{Type: PropertyType{String, Number}} + if !validate(schema, "Mon") { + t.Fatal("string value should match union string|number") + } + if !validate(schema, float64(0)) { + t.Fatal("number value should match union string|number") + } + if validate(schema, false) { + t.Fatal("bool should not match union string|number") + } +} From 2f8c3b4520c01e292a98f3420cfca5fae7b6b01a Mon Sep 17 00:00:00 2001 From: jesse-engineer <784909593@qq.com> Date: Thu, 16 Apr 2026 12:20:21 +0800 Subject: [PATCH 2/3] test(protocol): add regression for issue #198 ListTools JSON decode Cover union type arrays on nested properties and schemas containing $ref so ListToolsResult unmarshaling stays compatible with remote servers. Made-with: Cursor --- protocol/schema_validate_test.go | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/protocol/schema_validate_test.go b/protocol/schema_validate_test.go index f0f7719..74dc224 100644 --- a/protocol/schema_validate_test.go +++ b/protocol/schema_validate_test.go @@ -490,3 +490,37 @@ func Test_validate_unionType(t *testing.T) { t.Fatal("bool should not match union string|number") } } + +// TestIssue198_ListTools_jsonUnmarshal reproduces GitHub issue #198: remote schemas used +// JSON Schema type as an array (union) and sometimes $ref under items; decoding must not error. +func TestIssue198_ListTools_jsonUnmarshal(t *testing.T) { + payload := `{"tools":[` + + `{"name":"generate_heatmap_chart","inputSchema":{` + + `"type":"object","properties":{` + + `"data":{"type":"array","items":{` + + `"type":"object","properties":{` + + `"x":{"type":["string","number"]},` + + `"y":{"type":["string","number"]},` + + `"value":{"type":"number"}},"required":["x","y","value"],"additionalProperties":false` + + `}}},"required":["data"],"additionalProperties":false}},` + + `{"name":"generate_treemap_chart","inputSchema":{` + + `"type":"object","properties":{` + + `"data":{"type":"array","items":{` + + `"type":"object","properties":{` + + `"name":{"type":"string"},"value":{"type":"number"},` + + `"children":{"type":"array","items":{"$ref":"#/properties/data/items"}}},` + + `"required":["name","value"],"additionalProperties":false` + + `}}},"required":["data"],"additionalProperties":false}}` + + `]}` + var got ListToolsResult + if err := json.Unmarshal([]byte(payload), &got); err != nil { + t.Fatalf("unmarshal ListToolsResult: %v", err) + } + if len(got.Tools) != 2 { + t.Fatalf("tools len = %d", len(got.Tools)) + } + heat := got.Tools[0].InputSchema.Properties["data"].Items.Properties["x"] + if len(heat.Type) != 2 || heat.Type[0] != String || heat.Type[1] != Number { + t.Fatalf("heatmap x.Type = %#v", heat.Type) + } +} From 7deec56b65b63118ef6436291e6e057b37d9f1bc Mon Sep 17 00:00:00 2001 From: jesse-engineer <784909593@qq.com> Date: Thu, 16 Apr 2026 12:21:25 +0800 Subject: [PATCH 3/3] test(protocol): drop slices package for Go 1.18 CI Replace slices.Equal with a small equalPropertyType helper; std slices requires Go 1.21+. Made-with: Cursor --- protocol/schema_generate_test.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/protocol/schema_generate_test.go b/protocol/schema_generate_test.go index 9b37c7a..b2a8fc9 100644 --- a/protocol/schema_generate_test.go +++ b/protocol/schema_generate_test.go @@ -2,7 +2,6 @@ package protocol import ( "reflect" - "slices" "sort" "testing" ) @@ -403,6 +402,18 @@ func compareInputSchema(a, b *InputSchema) bool { return true } +func equalPropertyType(a, b PropertyType) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + // compareProperty Compare the contents of two Property structures func compareProperty(a, b *Property) bool { if a == nil && b == nil { @@ -411,7 +422,7 @@ func compareProperty(a, b *Property) bool { if a == nil || b == nil { return false } - if !slices.Equal(a.Type, b.Type) { + if !equalPropertyType(a.Type, b.Type) { return false } if a.Description != b.Description {