mcp: fix multi-select enum elicitation
This commit is contained in:
parent
8b526cdb2e
commit
40475813dd
2 changed files with 160 additions and 34 deletions
|
|
@ -710,8 +710,10 @@ func validateElicitProperty(propName string, propSchema *jsonschema.Schema) erro
|
||||||
return validateElicitNumberProperty(propName, propSchema)
|
return validateElicitNumberProperty(propName, propSchema)
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return validateElicitBooleanProperty(propName, propSchema)
|
return validateElicitBooleanProperty(propName, propSchema)
|
||||||
|
case "array":
|
||||||
|
return validateElicitArrayProperty(propName, propSchema)
|
||||||
default:
|
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)
|
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
|
// 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 propSchema.Extra != nil {
|
||||||
if enumNamesRaw, exists := propSchema.Extra["enumNames"]; exists {
|
if enumNamesRaw, exists := propSchema.Extra["enumNames"]; exists {
|
||||||
// Type check enumNames - should be a slice
|
// Type check enumNames - should be a slice
|
||||||
|
|
@ -739,6 +741,15 @@ func validateElicitStringProperty(propName string, propSchema *jsonschema.Schema
|
||||||
}
|
}
|
||||||
return nil
|
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
|
// Validate format if specified - only specific formats are allowed
|
||||||
if propSchema.Format != "" {
|
if propSchema.Format != "" {
|
||||||
|
|
@ -791,6 +802,53 @@ func validateElicitNumberProperty(propName string, propSchema *jsonschema.Schema
|
||||||
return nil
|
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.
|
// validateElicitBooleanProperty validates boolean-type properties.
|
||||||
func validateElicitBooleanProperty(propName string, propSchema *jsonschema.Schema) error {
|
func validateElicitBooleanProperty(propName string, propSchema *jsonschema.Schema) error {
|
||||||
return validateDefaultProperty[bool](propName, propSchema)
|
return validateDefaultProperty[bool](propName, propSchema)
|
||||||
|
|
|
||||||
142
mcp/mcp_test.go
142
mcp/mcp_test.go
|
|
@ -1095,7 +1095,7 @@ func TestElicitationSchemaValidation(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string enum",
|
name: "single select untitled enum",
|
||||||
schema: &jsonschema.Schema{
|
schema: &jsonschema.Schema{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Properties: map[string]*jsonschema.Schema{
|
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{
|
schema: &jsonschema.Schema{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Properties: map[string]*jsonschema.Schema{
|
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{
|
schema: &jsonschema.Schema{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Properties: map[string]*jsonschema.Schema{
|
Properties: map[string]*jsonschema.Schema{
|
||||||
|
|
@ -1138,22 +1138,62 @@ func TestElicitationSchemaValidation(t *testing.T) {
|
||||||
Type: "string",
|
Type: "string",
|
||||||
OneOf: []*jsonschema.Schema{
|
OneOf: []*jsonschema.Schema{
|
||||||
{
|
{
|
||||||
Const: anyPtr(map[string]string{
|
Const: anyPtr("high"),
|
||||||
"const": "high",
|
Title: "High Priority",
|
||||||
"title": "High Priority",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Const: anyPtr(map[string]string{
|
Const: anyPtr("medium"),
|
||||||
"const": "medium",
|
Title: "Medium Priority",
|
||||||
"title": "Medium Priority",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Const: anyPtr(map[string]string{
|
Const: anyPtr("low"),
|
||||||
"const": "low",
|
Title: "Low Priority",
|
||||||
"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"},
|
"config": {Type: "object"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedError: "elicit schema property \"config\" has unsupported type \"object\", 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 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",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "array without items",
|
name: "array without items",
|
||||||
|
|
@ -1230,7 +1260,27 @@ func TestElicitationSchemaValidation(t *testing.T) {
|
||||||
"items": {Type: "array"},
|
"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",
|
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",
|
name: "untyped property",
|
||||||
|
|
@ -1395,7 +1467,7 @@ func TestElicitationSchemaValidation(t *testing.T) {
|
||||||
"data": {},
|
"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",
|
Type: "string",
|
||||||
OneOf: []*jsonschema.Schema{
|
OneOf: []*jsonschema.Schema{
|
||||||
{
|
{
|
||||||
Const: anyPtr(map[string]string{
|
Const: anyPtr("high"),
|
||||||
"const": "high",
|
Title: "High Priority",
|
||||||
"title": "High Priority",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1478,10 +1548,8 @@ func TestElicitContentValidation(t *testing.T) {
|
||||||
Type: "string",
|
Type: "string",
|
||||||
OneOf: []*jsonschema.Schema{
|
OneOf: []*jsonschema.Schema{
|
||||||
{
|
{
|
||||||
Const: anyPtr(map[string]string{
|
Const: anyPtr("potato"),
|
||||||
"const": "potato",
|
Title: "Potato Priority",
|
||||||
"title": "Potato Priority",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue