mcp: treat pointers equivalently to non-pointers when deriving schema

As reported in #199 and #200, the fact that we return a possibly "null"
schema for pointer types breaks various clients, which expect schemas to
be of type "object". This is an unfortunate footgun. For now, assume
that the user wants us to treat pointers equivalently to non-pointers.
If we want to change this behavior in the future, we can do so behind an
option.

+ a test

Also fix the handling of nil results in the case where the output schema
is non-nil: we must provide structured content in this case. (This was
causing the test to fail).

Fixes #199
Fixes #200
This commit is contained in:
Robert Findley 2025-08-21 10:57:28 -04:00 • committed by GitHub
commit 43ad1eb8b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -189,31 +189,26 @@ func toolFor[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandle
// TODO(v0.3.0): test
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
var err error
tt := *t
tt.InputSchema = t.InputSchema
if tt.InputSchema == nil {
tt.InputSchema, err = jsonschema.For[In](nil)
if err != nil {
return nil, nil, fmt.Errorf("input schema: %w", err)
}
}
inputResolved, err := tt.InputSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
if err != nil {
return nil, nil, fmt.Errorf("resolving input schema: %w", err)
var inputResolved *jsonschema.Resolved
if _, err := setSchema[In](&tt.InputSchema, &inputResolved); err != nil {
return nil, nil, fmt.Errorf("input schema: %w", err)
}
if tt.OutputSchema == nil && reflect.TypeFor[Out]() != reflect.TypeFor[any]() {
tt.OutputSchema, err = jsonschema.For[Out](nil)
}
if err != nil {
return nil, nil, fmt.Errorf("output schema: %w", err)
}
var outputResolved *jsonschema.Resolved
if tt.OutputSchema != nil {
outputResolved, err = tt.OutputSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
// Handling for zero values:
//
// If Out is a pointer type and we've derived the output schema from its
// element type, use the zero value of its element type in place of a typed
// nil.
var (
elemZero any // only non-nil if Out is a pointer type
outputResolved *jsonschema.Resolved
)
if reflect.TypeFor[Out]() != reflect.TypeFor[any]() {
var err error
elemZero, err = setSchema[Out](&t.OutputSchema, &outputResolved)
if err != nil {
return nil, nil, fmt.Errorf("resolving output schema: %w", err)
return nil, nil, fmt.Errorf("output schema: %v", err)
}
}
@ -255,12 +250,54 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
res = &CallToolResult{}
}
res.StructuredContent = out
if elemZero != nil {
// Avoid typed nil, which will serialize as JSON null.
// Instead, use the zero value of the non-zero
var z Out
if any(out) == any(z) { // zero is only non-nil if Out is a pointer type
res.StructuredContent = elemZero
}
}
if tt.OutputSchema != nil && elemZero != nil {
res.StructuredContent = elemZero
}
return res, nil
}
return &tt, th, nil
}
// setSchema sets the schema and resolved schema corresponding to the type T.
//
// If sfield is nil, the schema is derived from T.
//
// Pointers are treated equivalently to non-pointers when deriving the schema.
// If an indirection occurred to derive the schema, a non-nil zero value is
// returned to be used in place of the typed nil zero value.
//
// Note that if sfield already holds a schema, zero will be nil even if T is a
// pointer: if the user provided the schema, they may have intentionally
// derived it from the pointer type, and handling of zero values is up to them.
//
// TODO(rfindley): we really shouldn't ever return 'null' results. Maybe we
// should have a jsonschema.Zero(schema) helper?
func setSchema[T any](sfield **jsonschema.Schema, rfield **jsonschema.Resolved) (zero any, err error) {
rt := reflect.TypeFor[T]()
if *sfield == nil {
if rt.Kind() == reflect.Pointer {
rt = rt.Elem()
zero = reflect.Zero(rt).Interface()
}
// TODO: we should be able to pass nil opts here.
*sfield, err = jsonschema.ForType(rt, &jsonschema.ForOptions{})
}
if err != nil {
return zero, err
}
*rfield, err = (*sfield).Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
return zero, err
}
// AddTool adds a tool and handler to the server.
//
// A shallow copy of the tool is made first.