Skip to content

Substitute zero value for typed-nil pointer functool output#557

Open
PratikDhanave wants to merge 2 commits into
microsoft:mainfrom
PratikDhanave:fix-functool-nil-pointer-output
Open

Substitute zero value for typed-nil pointer functool output#557
PratikDhanave wants to merge 2 commits into
microsoft:mainfrom
PratikDhanave:fix-functool-nil-pointer-output

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

A functool created with a pointer output typeHandlerFor[In, *Out] — can never return (nil, nil), even though that is the idiomatic Go way to signal "no result".

The output schema is derived from the dereferenced type (jsonformat.ForType dereferences, and the root object schema is deliberately non-nullable). A typed-nil *Out marshals to JSON null, and output normalization then validates null against a type: "object" root, which always fails:

normalizing output: normalizing: validation error: validating root:
type: <invalid reflect.Value> has type "null", want "object"

So the handler’s return value is unreachable and every call to a pointer-output tool errors.

The upstream modelcontextprotocol/go-sdk — which this package’s HandlerFor is explicitly modeled on — handles exactly this case: it computes an elemZero (non-nil only when Out is a pointer) and substitutes it for a typed-nil output "to avoid typed nil, which will serialize as JSON null," so validation against the object schema succeeds. functool did no such substitution — a parity gap.

Fix

In New, when Out is a pointer type, precompute the zero element value and substitute it when the handler returns a typed nil:

var elemZero Out
outType := reflect.TypeFor[Out]()
hasPointerOut := outType != nil && outType.Kind() == reflect.Pointer
if hasPointerOut {
    elemZero = reflect.New(outType.Elem()).Interface().(Out)
}
...
if hasPointerOut && reflect.ValueOf(out).IsNil() {
    out = elemZero
}

Value-typed outputs are unaffected.

Test

TestFuncTool_PointerOutputNilReturnsZeroValue builds a tool whose handler returns (*Out)(nil) and asserts Call succeeds with a zero-value *Out. It fails on the old code (the null/object validation error) and passes with the fix.

A functool built with a pointer output type (HandlerFor[In, *Out]) could
never return (nil, nil): the output schema is derived from the
dereferenced, non-nullable object type, but a typed-nil pointer marshals
to JSON null, so output normalization always failed with
'has type "null", want "object"'. Every call to such a tool errored,
regardless of the handler's result.

When Out is a pointer type and the handler returns a typed nil, substitute
the zero value of the element type before normalizing, mirroring the MCP
go-sdk's HandlerFor (which this package's HandlerFor is modeled on).
@PratikDhanave
PratikDhanave requested a review from a team as a code owner July 19, 2026 11:25
Copilot AI review requested due to automatic review settings July 19, 2026 11:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a functool.HandlerFor parity gap for pointer-typed outputs (Out = *T) where a handler returning a typed-nil pointer ((nil, nil)) previously serialized to JSON null and then failed validation against the non-nullable, object-rooted output schema.

Changes:

  • Precomputes a zero element pointer for pointer Out types and substitutes it when the handler returns a typed-nil pointer, avoiding null output normalization failures.
  • Adds a regression test ensuring (*Out)(nil) output is accepted and becomes a non-nil zero-value pointer.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
tool/functool/func.go Adds typed-nil pointer output substitution logic during handler execution.
tool/functool/func_test.go Adds regression test covering pointer output typed-nil behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tool/functool/func.go
Comment on lines +67 to +70
hasPointerOut := outType != nil && outType.Kind() == reflect.Pointer
if hasPointerOut {
elemZero = reflect.New(outType.Elem()).Interface().(Out)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a follow-up commit — converts to outType before the assertion (reflect.New(outType.Elem()).Convert(outType).Interface().(Out)), with a named-pointer regression test.

reflect.New(outType.Elem()) yields an unnamed *Elem, which is not
assertable to a named pointer output type (e.g. type P *T). Convert to
the output type before the assertion so a functool with a named pointer
output does not panic at construction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants