diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 86c05502..66c2cd44 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -2244,4 +2244,45 @@ func TestToolErrorMiddleware(t *testing.T) { } } +func TestSetErrorPreservesContent(t *testing.T) { + for _, tt := range []struct { + name string + content []Content + err error + wantContent string + }{ + { + name: "nil content", + err: errors.New("internal failure"), + wantContent: "internal failure", + }, + { + name: "empty slice content", + content: []Content{}, + err: errors.New("internal failure"), + wantContent: "internal failure", + }, + { + name: "existing content preserved", + content: []Content{&TextContent{Text: "user-friendly msg"}}, + err: errors.New("db timeout"), + wantContent: "user-friendly msg", + }, + } { + t.Run(tt.name, func(t *testing.T) { + res := CallToolResult{Content: tt.content} + res.SetError(tt.err) + if !res.IsError { + t.Fatal("want IsError=true") + } + if got := res.Content[0].(*TextContent).Text; got != tt.wantContent { + t.Errorf("Content text = %q, want %q", got, tt.wantContent) + } + if got := res.GetError(); got != tt.err { + t.Errorf("GetError() = %v, want %v", got, tt.err) + } + }) + } +} + var ctrCmpOpts = []cmp.Option{cmp.AllowUnexported(CallToolResult{})} diff --git a/mcp/protocol.go b/mcp/protocol.go index 837ce784..c15ef030 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -16,6 +16,7 @@ import ( "maps" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" ) // Optional annotations for the client. The client can use annotations to inform @@ -119,10 +120,25 @@ 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. +// seterroroverwrite is a compatibility parameter that restores the pre-1.5.0 +// behavior of [CallToolResult.SetError], where Content was always overwritten +// with the error text. See the documentation for the mcpgodebug package for +// instructions on how to enable it. +// The option will be removed in the 1.7.0 version of the SDK. +var seterroroverwrite = mcpgodebug.Value("seterroroverwrite") + +// 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. +// +// To restore the previous behavior where Content was always overwritten, +// set MCPGODEBUG=seterroroverwrite=1. func (r *CallToolResult) SetError(err error) { - r.Content = []Content{&TextContent{Text: err.Error()}} + if len(r.Content) == 0 || seterroroverwrite == "1" { + r.Content = []Content{&TextContent{Text: err.Error()}} + } r.IsError = true r.err = err }