Skip to content

Commit 272d160

Browse files
SamMorrowDrumsCopilotblackwell-systems
authored
fix: return isError for argument validation failures (#2511)
* refactor: simplify NewServerTool naming - Rename NewServerToolWithRawContextHandler -> NewServerTool. This is the preferred constructor for raw mcp.ToolHandler tools because it avoids creating closures at registration time, which matters for per-request servers that re-register all tools on every request. - Rename deprecated generic NewServerTool[In, Out] -> NewServerToolWithDeps to free up the simpler name and make its closure-based nature explicit. The dynamic tools package is the only legitimate user of this constructor because DynamicToolDependencies differs from the standard ToolDependencies. - Remove deprecated NewServerToolFromHandler. Its only callers can use the new NewServerTool directly via context-injected deps. - Update all call sites in dependencies.go, dynamic_tools.go, and registry_test.go. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: blackwell-systems <236632453+blackwell-systems@users.noreply.github.com>
1 parent 970155a commit 272d160

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)