mcp: preserve existing Content in SetError (#864)
SetError now only populates the Content field when it has not already been populated. This allows callers to provide a user-friendly message while still recording the underlying error for middleware inspection via GetError. The len check (rather than nil check) ensures both nil and empty-slice Content are treated as "not yet populated." Fixes #858
This commit is contained in:
parent
27f29c1cef
commit
34a19dff46
2 changed files with 60 additions and 3 deletions
|
|
@ -2228,4 +2228,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{})}
|
var ctrCmpOpts = []cmp.Option{cmp.AllowUnexported(CallToolResult{})}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"maps"
|
"maps"
|
||||||
|
|
||||||
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
|
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
|
// Optional annotations for the client. The client can use annotations to inform
|
||||||
|
|
@ -113,10 +114,25 @@ type CallToolResult struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetError sets the error for the tool result and populates the Content field
|
// seterroroverwrite is a compatibility parameter that restores the pre-1.6.0
|
||||||
// with the error text. It also sets IsError to true.
|
// 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.8.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) {
|
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.IsError = true
|
||||||
r.err = err
|
r.err = err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue