diff --git a/mcp/server.go b/mcp/server.go index a6bbdea7..c1ded2a2 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -443,8 +443,10 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa // Marshal the output and put the RawMessage in the StructuredContent field. // Skip when the handler asked for input (multi round-trip): content and // an input-required result are mutually exclusive on the wire. + // Skip for an error result too: out is not a tool result then, and + // its zero value may not satisfy the output schema. var outval any = out - if asksForInput(res) { + if asksForInput(res) || res.IsError { outval = nil } else if elemZero != nil { // Avoid typed nil, which will serialize as JSON null. diff --git a/mcp/tool_test.go b/mcp/tool_test.go index b47b6505..1e779f40 100644 --- a/mcp/tool_test.go +++ b/mcp/tool_test.go @@ -225,6 +225,53 @@ func TestToolErrorHandling(t *testing.T) { }) } +func TestToolErrorResultOmitsOutput(t *testing.T) { + type out struct { + Status string `json:"status"` + } + server := NewServer(testImpl, nil) + AddTool(server, &Tool{ + Name: "failing", + OutputSchema: &jsonschema.Schema{ + Type: "object", + Required: []string{"status"}, + Properties: map[string]*jsonschema.Schema{"status": {Type: "string", Enum: []any{"ok"}}}, + }, + }, func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, out, error) { + return &CallToolResult{ + IsError: true, + Content: []Content{&TextContent{Text: "no such item"}}, + }, out{}, nil + }) + + ct, st := NewInMemoryTransports() + if _, err := server.Connect(context.Background(), st, nil); err != nil { + t.Fatal(err) + } + cs, err := NewClient(testImpl, nil).Connect(context.Background(), ct, nil) + if err != nil { + t.Fatal(err) + } + defer cs.Close() + + res, err := cs.CallTool(context.Background(), &CallToolParams{Name: "failing", Arguments: map[string]any{}}) + if err != nil { + t.Fatalf("CallTool: %v", err) + } + if !res.IsError { + t.Error("IsError = false, want true") + } + if res.StructuredContent != nil { + t.Errorf("StructuredContent = %v, want nil", res.StructuredContent) + } + if len(res.Content) != 1 { + t.Fatalf("got %d content blocks, want 1", len(res.Content)) + } + if tc, ok := res.Content[0].(*TextContent); !ok || tc.Text != "no such item" { + t.Errorf("Content[0] = %v, want the handler's error text", res.Content[0]) + } +} + func TestValidateToolName(t *testing.T) { t.Run("valid", func(t *testing.T) { validTests := []struct {