Skip to content

Commit ac0f024

Browse files
SamMorrowDrumsblackwell-systemsCopilot
committed
fix: return isError for argument validation failures
When tool argument unmarshalling fails (wrong types, malformed JSON), return a CallToolResult with IsError: true instead of a Go error. Returning a Go error is converted by the SDK into a JSON-RPC protocol error (-32603), which is invisible to agents and prevents self-correction. Returning IsError: true with the validation message lets agents see the problem and retry with corrected arguments. Affects: - NewServerToolWithDeps (was NewServerTool prior to the rename in #2510) - NewServerToolWithContextHandler Fixes #1952. Re-applies #2488 by @blackwell-systems on top of the NewServerTool rename. Co-authored-by: blackwell-systems <236632453+blackwell-systems@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5b022ba commit ac0f024

2 files changed

Lines changed: 131 additions & 2 deletions

File tree

pkg/inventory/server_tool.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package inventory
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67

78
"github.com/github/github-mcp-server/pkg/octicons"
89
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -133,7 +134,12 @@ func NewServerToolWithDeps[In any, Out any](tool mcp.Tool, toolset ToolsetMetada
133134
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
134135
var arguments In
135136
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
136-
return nil, err
137+
return &mcp.CallToolResult{
138+
Content: []mcp.Content{
139+
&mcp.TextContent{Text: fmt.Sprintf("invalid arguments: %s", err)},
140+
},
141+
IsError: true,
142+
}, nil
137143
}
138144
resp, _, err := typedHandler(ctx, req, arguments)
139145
return resp, err
@@ -157,7 +163,12 @@ func NewServerToolWithContextHandler[In any, Out any](tool mcp.Tool, toolset Too
157163
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
158164
var arguments In
159165
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
160-
return nil, err
166+
return &mcp.CallToolResult{
167+
Content: []mcp.Content{
168+
&mcp.TextContent{Text: fmt.Sprintf("invalid arguments: %s", err)},
169+
},
170+
IsError: true,
171+
}, nil
161172
}
162173
resp, _, err := handler(ctx, req, arguments)
163174
return resp, err

pkg/inventory/server_tool_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package inventory
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestNewServerToolWithDeps_InvalidArguments_ReturnsIsError(t *testing.T) {
14+
type expectedArgs struct {
15+
Owner string `json:"owner"`
16+
Repo string `json:"repo"`
17+
}
18+
19+
tool := NewServerToolWithDeps(
20+
mcp.Tool{Name: "test_tool"},
21+
testToolsetMetadata("test"),
22+
func(_ any) mcp.ToolHandlerFor[expectedArgs, *mcp.CallToolResult] {
23+
return func(_ context.Context, _ *mcp.CallToolRequest, _ expectedArgs) (*mcp.CallToolResult, *mcp.CallToolResult, error) {
24+
t.Fatal("handler should not be called with invalid arguments")
25+
return nil, nil, nil
26+
}
27+
},
28+
)
29+
30+
handler := tool.HandlerFunc(nil)
31+
32+
badArgs, _ := json.Marshal(map[string]any{"owner": 12345, "repo": true})
33+
result, err := handler(context.Background(), &mcp.CallToolRequest{
34+
Params: &mcp.CallToolParamsRaw{
35+
Name: "test_tool",
36+
Arguments: badArgs,
37+
},
38+
})
39+
40+
require.NoError(t, err)
41+
require.NotNil(t, result)
42+
assert.True(t, result.IsError)
43+
assert.Len(t, result.Content, 1)
44+
textContent, ok := result.Content[0].(*mcp.TextContent)
45+
require.True(t, ok)
46+
assert.Contains(t, textContent.Text, "invalid arguments")
47+
}
48+
49+
func TestNewServerToolWithContextHandler_InvalidArguments_ReturnsIsError(t *testing.T) {
50+
type expectedArgs struct {
51+
Query string `json:"query"`
52+
Limit int `json:"limit"`
53+
}
54+
55+
tool := NewServerToolWithContextHandler(
56+
mcp.Tool{Name: "test_context_tool"},
57+
testToolsetMetadata("test"),
58+
func(_ context.Context, _ *mcp.CallToolRequest, _ expectedArgs) (*mcp.CallToolResult, any, error) {
59+
t.Fatal("handler should not be called with invalid arguments")
60+
return nil, nil, nil
61+
},
62+
)
63+
64+
handler := tool.HandlerFunc(nil)
65+
66+
result, err := handler(context.Background(), &mcp.CallToolRequest{
67+
Params: &mcp.CallToolParamsRaw{
68+
Name: "test_context_tool",
69+
Arguments: json.RawMessage(`{not valid json`),
70+
},
71+
})
72+
73+
require.NoError(t, err)
74+
require.NotNil(t, result)
75+
assert.True(t, result.IsError)
76+
assert.Len(t, result.Content, 1)
77+
textContent, ok := result.Content[0].(*mcp.TextContent)
78+
require.True(t, ok)
79+
assert.Contains(t, textContent.Text, "invalid arguments")
80+
}
81+
82+
func TestNewServerToolWithDeps_ValidArguments_Succeeds(t *testing.T) {
83+
type expectedArgs struct {
84+
Owner string `json:"owner"`
85+
Repo string `json:"repo"`
86+
}
87+
88+
tool := NewServerToolWithDeps(
89+
mcp.Tool{Name: "test_tool"},
90+
testToolsetMetadata("test"),
91+
func(_ any) mcp.ToolHandlerFor[expectedArgs, *mcp.CallToolResult] {
92+
return func(_ context.Context, _ *mcp.CallToolRequest, args expectedArgs) (*mcp.CallToolResult, *mcp.CallToolResult, error) {
93+
return &mcp.CallToolResult{
94+
Content: []mcp.Content{
95+
&mcp.TextContent{Text: "success: " + args.Owner + "/" + args.Repo},
96+
},
97+
}, nil, nil
98+
}
99+
},
100+
)
101+
102+
handler := tool.HandlerFunc(nil)
103+
104+
goodArgs, _ := json.Marshal(map[string]any{"owner": "octocat", "repo": "hello-world"})
105+
result, err := handler(context.Background(), &mcp.CallToolRequest{
106+
Params: &mcp.CallToolParamsRaw{
107+
Name: "test_tool",
108+
Arguments: goodArgs,
109+
},
110+
})
111+
112+
require.NoError(t, err)
113+
require.NotNil(t, result)
114+
assert.False(t, result.IsError)
115+
textContent, ok := result.Content[0].(*mcp.TextContent)
116+
require.True(t, ok)
117+
assert.Equal(t, "success: octocat/hello-world", textContent.Text)
118+
}

0 commit comments

Comments
 (0)