diff --git a/mcp/client.go b/mcp/client.go index 96f1f82..bc81c1e 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -710,8 +710,10 @@ func validateElicitProperty(propName string, propSchema *jsonschema.Schema) erro return validateElicitNumberProperty(propName, propSchema) case "boolean": return validateElicitBooleanProperty(propName, propSchema) + case "array": + return validateElicitArrayProperty(propName, propSchema) default: - return fmt.Errorf("elicit schema property %q has unsupported type %q, only string, number, integer, and boolean are allowed", propName, propSchema.Type) + return fmt.Errorf("elicit schema property %q has unsupported type %q, only string, number, integer, boolean, and array are allowed", propName, propSchema.Type) } } @@ -724,7 +726,7 @@ func validateElicitStringProperty(propName string, propSchema *jsonschema.Schema return fmt.Errorf("elicit schema property %q has enum values but type is %q, enums are only supported for string type", propName, propSchema.Type) } // Enum values themselves are validated by the JSON schema library - // Validate enumNames if present - must match enum length + // Validate legacy enumNames if present - must match enum length. if propSchema.Extra != nil { if enumNamesRaw, exists := propSchema.Extra["enumNames"]; exists { // Type check enumNames - should be a slice @@ -739,6 +741,15 @@ func validateElicitStringProperty(propName string, propSchema *jsonschema.Schema } return nil } + // Handle new style of titled enums. + if propSchema.OneOf != nil { + for _, entry := range propSchema.OneOf { + if err := validateTitledEnumEntry(entry); err != nil { + return fmt.Errorf("elicit schema property %q oneOf has invalid entry: %v", propName, err) + } + } + return nil + } // Validate format if specified - only specific formats are allowed if propSchema.Format != "" { @@ -791,6 +802,53 @@ func validateElicitNumberProperty(propName string, propSchema *jsonschema.Schema return nil } +// validateElicitArrayProperty validates multi-select enum properties. +func validateElicitArrayProperty(propName string, propSchema *jsonschema.Schema) error { + if propSchema.Items == nil { + return fmt.Errorf("elicit schema property %q is array but missing 'items' definition", propName) + } + + items := propSchema.Items + switch items.Type { + case "string": + // Untitled enums. + if items.Enum == nil { + return fmt.Errorf("elicit schema property %q items must specify enum for untitled enums", propName) + } + return nil + case "": + // Titled enums. + if len(items.AnyOf) == 0 { + return fmt.Errorf("elicit schema property %q items must specify anyOf for titled enums", propName) + } + for _, entry := range items.AnyOf { + if err := validateTitledEnumEntry(entry); err != nil { + return fmt.Errorf("elicit schema property %q items has invalid entry: %v", propName, err) + } + } + return nil + default: + return fmt.Errorf("elicit schema property %q items have unsupported type %q", propName, items.Type) + } +} + +func validateTitledEnumEntry(entry *jsonschema.Schema) error { + if entry.Const == nil { + return fmt.Errorf("const is required for titled enum entries") + } + constVal, ok := (*entry.Const).(string) + if !ok { + return fmt.Errorf("const must be a string for titled enum entries") + } + if constVal == "" { + return fmt.Errorf("const cannot be empty for titled enum entries") + } + if entry.Title == "" { + return fmt.Errorf("title is required for titled enum entries") + } + return nil +} + // validateElicitBooleanProperty validates boolean-type properties. func validateElicitBooleanProperty(propName string, propSchema *jsonschema.Schema) error { return validateDefaultProperty[bool](propName, propSchema) diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 71a1dcc..86c0550 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -1095,7 +1095,7 @@ func TestElicitationSchemaValidation(t *testing.T) { }, }, { - name: "string enum", + name: "single select untitled enum", schema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -1111,7 +1111,7 @@ func TestElicitationSchemaValidation(t *testing.T) { }, }, { - name: "enum with matching enumNames", + name: "legacy titled enum", schema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -1130,7 +1130,7 @@ func TestElicitationSchemaValidation(t *testing.T) { }, }, { - name: "enum with enum schema", + name: "single select titled enum", schema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -1138,22 +1138,62 @@ func TestElicitationSchemaValidation(t *testing.T) { Type: "string", OneOf: []*jsonschema.Schema{ { - Const: anyPtr(map[string]string{ - "const": "high", - "title": "High Priority", - }), + Const: anyPtr("high"), + Title: "High Priority", }, { - Const: anyPtr(map[string]string{ - "const": "medium", - "title": "Medium Priority", - }), + Const: anyPtr("medium"), + Title: "Medium Priority", }, { - Const: anyPtr(map[string]string{ - "const": "low", - "title": "Low Priority", - }), + Const: anyPtr("low"), + Title: "Low Priority", + }, + }, + }, + }, + }, + }, + { + name: "multi select untitled enum", + schema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "status": { + Type: "array", + Items: &jsonschema.Schema{ + Type: "string", + Enum: []any{ + "active", + "inactive", + "pending", + }, + }, + }, + }, + }, + }, + { + name: "multi select titled enum", + schema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "priority": { + Type: "array", + Items: &jsonschema.Schema{ + AnyOf: []*jsonschema.Schema{ + { + Const: anyPtr("high"), + Title: "High Priority", + }, + { + Const: anyPtr("medium"), + Title: "Medium Priority", + }, + { + Const: anyPtr("low"), + Title: "Low Priority", + }, }, }, }, @@ -1210,17 +1250,7 @@ func TestElicitationSchemaValidation(t *testing.T) { "config": {Type: "object"}, }, }, - expectedError: "elicit schema property \"config\" has unsupported type \"object\", only string, number, integer, and boolean are allowed", - }, - { - name: "array property", - schema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "tags": {Type: "array", Items: &jsonschema.Schema{Type: "string"}}, - }, - }, - expectedError: "elicit schema property \"tags\" has unsupported type \"array\", only string, number, integer, and boolean are allowed", + expectedError: "elicit schema property \"config\" has unsupported type \"object\", only string, number, integer, boolean, and array are allowed", }, { name: "array without items", @@ -1230,7 +1260,27 @@ func TestElicitationSchemaValidation(t *testing.T) { "items": {Type: "array"}, }, }, - expectedError: "elicit schema property \"items\" has unsupported type \"array\", only string, number, integer, and boolean are allowed", + expectedError: "elicit schema property \"items\" is array but missing 'items' definition", + }, + { + name: "array with integer items", + schema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "items": {Type: "array", Items: &jsonschema.Schema{Type: "integer"}}, + }, + }, + expectedError: "elicit schema property \"items\" items have unsupported type \"integer\"", + }, + { + name: "array of generic strings", + schema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "items": {Type: "array", Items: &jsonschema.Schema{Type: "string"}}, + }, + }, + expectedError: "elicit schema property \"items\" items must specify enum for untitled enums", }, { name: "unsupported string format", @@ -1385,7 +1435,29 @@ func TestElicitationSchemaValidation(t *testing.T) { }, }, }, - expectedError: "elicit schema property \"priority\" has unsupported type \"\", only string, number, integer, and boolean are allowed", + expectedError: "elicit schema property \"priority\" has unsupported type \"\", only string, number, integer, boolean, and array are allowed", + }, + { + name: "titled enum without const", + schema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "priority": { + Type: "string", + OneOf: []*jsonschema.Schema{ + { + Const: anyPtr("high"), + Title: "High Priority", + }, + { + Type: "string", + Title: "Other Priority", + }, + }, + }, + }, + }, + expectedError: "elicit schema property \"priority\" oneOf has invalid entry: const is required for titled enum entries", }, { name: "untyped property", @@ -1395,7 +1467,7 @@ func TestElicitationSchemaValidation(t *testing.T) { "data": {}, }, }, - expectedError: "elicit schema property \"data\" has unsupported type \"\", only string, number, integer, and boolean are allowed", + expectedError: "elicit schema property \"data\" has unsupported type \"\", only string, number, integer, boolean, and array are allowed", }, } @@ -1458,10 +1530,8 @@ func TestElicitContentValidation(t *testing.T) { Type: "string", OneOf: []*jsonschema.Schema{ { - Const: anyPtr(map[string]string{ - "const": "high", - "title": "High Priority", - }), + Const: anyPtr("high"), + Title: "High Priority", }, }, }, @@ -1478,10 +1548,8 @@ func TestElicitContentValidation(t *testing.T) { Type: "string", OneOf: []*jsonschema.Schema{ { - Const: anyPtr(map[string]string{ - "const": "potato", - "title": "Potato Priority", - }), + Const: anyPtr("potato"), + Title: "Potato Priority", }, }, },