Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2244,4 +2244,54 @@ func TestToolErrorMiddleware(t *testing.T) {
}
}

func TestSetErrorPreservesContent(t *testing.T) {
t.Run("empty content is populated", func(t *testing.T) {
var res CallToolResult
res.SetError(errors.New("internal failure"))
if !res.IsError {
t.Fatal("want IsError=true")
}
text := res.Content[0].(*TextContent).Text
if text != "internal failure" {
t.Errorf("got %q, want %q", text, "internal failure")
}
if res.GetError() == nil || res.GetError().Error() != "internal failure" {
t.Errorf("GetError() = %v, want 'internal failure'", res.GetError())
}
})

t.Run("empty slice content is populated", func(t *testing.T) {
res := CallToolResult{
Content: []Content{},
}
res.SetError(errors.New("internal failure"))
if !res.IsError {
t.Fatal("want IsError=true")
}
text := res.Content[0].(*TextContent).Text
if text != "internal failure" {
t.Errorf("got %q, want %q", text, "internal failure")
}
})

t.Run("existing content is preserved", func(t *testing.T) {
res := CallToolResult{
Content: []Content{&TextContent{Text: "Something went wrong, please try again."}},
}
res.SetError(errors.New("db timeout on query SELECT * FROM users"))
if !res.IsError {
t.Fatal("want IsError=true")
}
// User-facing content should be preserved.
text := res.Content[0].(*TextContent).Text
if text != "Something went wrong, please try again." {
t.Errorf("Content was overwritten: got %q", text)
}
// Internal error should still be accessible via GetError.
if res.GetError() == nil || res.GetError().Error() != "db timeout on query SELECT * FROM users" {
t.Errorf("GetError() = %v, want 'db timeout on query SELECT * FROM users'", res.GetError())
}
})
}

var ctrCmpOpts = []cmp.Option{cmp.AllowUnexported(CallToolResult{})}
11 changes: 8 additions & 3 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,15 @@ type CallToolResult struct {
err error
}

// SetError sets the error for the tool result and populates the Content field
// with the error text. It also sets IsError to true.
// SetError sets the error for the tool result and sets IsError to true.
// If Content has not already been populated, it is set to the error text.
// If Content has already been populated, it is left unchanged, allowing callers
// to provide a user-friendly message while still recording the underlying error
// for inspection via [GetError] in server middleware.
func (r *CallToolResult) SetError(err error) {
r.Content = []Content{&TextContent{Text: err.Error()}}
if len(r.Content) == 0 {
r.Content = []Content{&TextContent{Text: err.Error()}}
}
r.IsError = true
r.err = err
}
Expand Down
Loading