Skip to content

Commit 65109a7

Browse files
committed
fix: add validation tests
1 parent 27e342d commit 65109a7

File tree

2 files changed

+438
-27
lines changed

2 files changed

+438
-27
lines changed

mcp/client.go

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package mcp
66

77
import (
88
"context"
9+
"encoding/json"
910
"fmt"
1011
"iter"
1112
"slices"
@@ -267,42 +268,143 @@ func (c *Client) elicit(ctx context.Context, cs *ClientSession, params *ElicitPa
267268
// TODO: wrap or annotate this error? Pick a standard code?
268269
return nil, jsonrpc2.NewError(CodeUnsupportedMethod, "client does not support elicitation")
269270
}
270-
271+
271272
// Validate that the requested schema only contains top-level properties without nesting
272273
if err := validateElicitSchema(params.RequestedSchema); err != nil {
273274
return nil, jsonrpc2.NewError(CodeInvalidParams, err.Error())
274275
}
275-
276+
276277
return c.opts.ElicitationHandler(ctx, cs, params)
277278
}
278279

279-
// validateElicitSchema validates that the schema only contains top-level properties without nesting.
280+
// validateElicitSchema validates that the schema conforms to MCP elicitation schema requirements.
281+
// Per the MCP specification, elicitation schemas are limited to flat objects with primitive properties only.
280282
func validateElicitSchema(schema *jsonschema.Schema) error {
281283
if schema == nil {
282284
return nil // nil schema is allowed
283285
}
284-
286+
287+
// The root schema must be of type "object" if specified
288+
if schema.Type != "" && schema.Type != "object" {
289+
return fmt.Errorf("elicit schema must be of type 'object', got %q", schema.Type)
290+
}
291+
285292
// Check if the schema has properties
286293
if schema.Properties != nil {
287294
for propName, propSchema := range schema.Properties {
288295
if propSchema == nil {
289296
continue
290297
}
291-
292-
// Check if this property has nested properties (not allowed)
293-
if propSchema.Properties != nil && len(propSchema.Properties) > 0 {
294-
return fmt.Errorf("elicit schema property %q contains nested properties, only top-level properties are allowed", propName)
298+
299+
if err := validateElicitProperty(propName, propSchema); err != nil {
300+
return err
295301
}
296-
297-
// Also check Items for arrays that might contain nested objects
298-
if propSchema.Items != nil {
299-
if propSchema.Items.Properties != nil && len(propSchema.Items.Properties) > 0 {
300-
return fmt.Errorf("elicit schema property %q contains array items with nested properties, only top-level properties are allowed", propName)
302+
}
303+
}
304+
305+
return nil
306+
}
307+
308+
// validateElicitProperty validates a single property in an elicitation schema.
309+
func validateElicitProperty(propName string, propSchema *jsonschema.Schema) error {
310+
// Check if this property has nested properties (not allowed)
311+
if propSchema.Properties != nil && len(propSchema.Properties) > 0 {
312+
return fmt.Errorf("elicit schema property %q contains nested properties, only primitive properties are allowed", propName)
313+
}
314+
315+
// Validate based on the property type - only primitives are supported
316+
switch propSchema.Type {
317+
case "string":
318+
return validateElicitStringProperty(propName, propSchema)
319+
case "number", "integer":
320+
return validateElicitNumberProperty(propName, propSchema)
321+
case "boolean":
322+
return validateElicitBooleanProperty(propName, propSchema)
323+
default:
324+
return fmt.Errorf("elicit schema property %q has unsupported type %q, only string, number, integer, and boolean are allowed", propName, propSchema.Type)
325+
}
326+
}
327+
328+
// validateElicitStringProperty validates string-type properties, including enums.
329+
func validateElicitStringProperty(propName string, propSchema *jsonschema.Schema) error {
330+
// Handle enum validation (enums are a special case of strings)
331+
if len(propSchema.Enum) > 0 {
332+
// Enums must be string type (or untyped which defaults to string)
333+
if propSchema.Type != "" && propSchema.Type != "string" {
334+
return fmt.Errorf("elicit schema property %q has enum values but type is %q, enums are only supported for string type", propName, propSchema.Type)
335+
}
336+
// Enum values themselves are validated by the JSON schema library
337+
// Validate enumNames if present - must match enum length
338+
if propSchema.Extra != nil {
339+
if enumNamesRaw, exists := propSchema.Extra["enumNames"]; exists {
340+
// Type check enumNames - should be a slice
341+
if enumNamesSlice, ok := enumNamesRaw.([]interface{}); ok {
342+
if len(enumNamesSlice) != len(propSchema.Enum) {
343+
return fmt.Errorf("elicit schema property %q has %d enum values but %d enumNames, they must match", propName, len(propSchema.Enum), len(enumNamesSlice))
344+
}
345+
} else {
346+
return fmt.Errorf("elicit schema property %q has invalid enumNames type, must be an array", propName)
301347
}
302348
}
303349
}
350+
return nil
351+
}
352+
353+
// Validate format if specified - only specific formats are allowed
354+
if propSchema.Format != "" {
355+
allowedFormats := map[string]bool{
356+
"email": true,
357+
"uri": true,
358+
"date": true,
359+
"date-time": true,
360+
}
361+
if !allowedFormats[propSchema.Format] {
362+
return fmt.Errorf("elicit schema property %q has unsupported format %q, only email, uri, date, and date-time are allowed", propName, propSchema.Format)
363+
}
364+
}
365+
366+
// Validate minLength constraint if specified
367+
if propSchema.MinLength != nil {
368+
if *propSchema.MinLength < 0 {
369+
return fmt.Errorf("elicit schema property %q has invalid minLength %d, must be non-negative", propName, *propSchema.MinLength)
370+
}
304371
}
305-
372+
373+
// Validate maxLength constraint if specified
374+
if propSchema.MaxLength != nil {
375+
if *propSchema.MaxLength < 0 {
376+
return fmt.Errorf("elicit schema property %q has invalid maxLength %d, must be non-negative", propName, *propSchema.MaxLength)
377+
}
378+
// Check that maxLength >= minLength if both are specified
379+
if propSchema.MinLength != nil && *propSchema.MaxLength < *propSchema.MinLength {
380+
return fmt.Errorf("elicit schema property %q has maxLength %d less than minLength %d", propName, *propSchema.MaxLength, *propSchema.MinLength)
381+
}
382+
}
383+
384+
return nil
385+
}
386+
387+
// validateElicitNumberProperty validates number and integer-type properties.
388+
func validateElicitNumberProperty(propName string, propSchema *jsonschema.Schema) error {
389+
if propSchema.Minimum != nil && propSchema.Maximum != nil {
390+
if *propSchema.Maximum < *propSchema.Minimum {
391+
return fmt.Errorf("elicit schema property %q has maximum %g less than minimum %g", propName, *propSchema.Maximum, *propSchema.Minimum)
392+
}
393+
}
394+
395+
return nil
396+
}
397+
398+
// validateElicitBooleanProperty validates boolean-type properties.
399+
func validateElicitBooleanProperty(propName string, propSchema *jsonschema.Schema) error {
400+
// Validate default value if specified - must be a valid boolean
401+
if propSchema.Default != nil {
402+
var defaultValue bool
403+
if err := json.Unmarshal(propSchema.Default, &defaultValue); err != nil {
404+
return fmt.Errorf("elicit schema property %q has invalid default value, must be a boolean: %v", propName, err)
405+
}
406+
}
407+
306408
return nil
307409
}
308410

0 commit comments

Comments
 (0)