Skip to content

Commit a484e76

Browse files
advancedresearcharraycursoragent
andcommitted
fix: accept omitted tool arguments for zero-param tools like get_me
When MCP clients omit the arguments field on tools/call, the typed tool wrapper received nil RawMessage and failed JSON unmarshaling before the handler ran. Coerce nil or empty arguments to {} so zero-parameter tools such as get_me work as documented. Closes #2587 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 457f599 commit a484e76

4 files changed

Lines changed: 148 additions & 1 deletion

File tree

pkg/github/context_tools_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,48 @@ func Test_GetMe(t *testing.T) {
139139
}
140140
}
141141

142+
func Test_GetMe_OmittedArguments(t *testing.T) {
143+
t.Parallel()
144+
145+
serverTool := GetMe(translations.NullTranslationHelper)
146+
147+
mockUser := &github.User{
148+
Login: github.Ptr("testuser"),
149+
Name: github.Ptr("Test User"),
150+
HTMLURL: github.Ptr("https://github.com/testuser"),
151+
CreatedAt: &github.Timestamp{Time: time.Now()},
152+
}
153+
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
154+
GetUser: mockResponse(t, http.StatusOK, mockUser),
155+
})
156+
deps := BaseDeps{Client: mustNewGHClient(t, mockedClient), Obsv: stubExporters()}
157+
handler := serverTool.Handler(deps)
158+
159+
tests := []struct {
160+
name string
161+
arguments json.RawMessage
162+
}{
163+
{name: "nil arguments", arguments: nil},
164+
{name: "empty byte slice", arguments: json.RawMessage{}},
165+
{name: "explicit empty object", arguments: json.RawMessage(`{}`)},
166+
}
167+
168+
for _, tc := range tests {
169+
t.Run(tc.name, func(t *testing.T) {
170+
request := createMCPRequestWithRawArguments(tc.arguments)
171+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
172+
require.NoError(t, err)
173+
require.False(t, result.IsError)
174+
175+
textContent := getTextResult(t, result)
176+
var returnedUser MinimalUser
177+
err = json.Unmarshal([]byte(textContent.Text), &returnedUser)
178+
require.NoError(t, err)
179+
assert.Equal(t, "testuser", returnedUser.Login)
180+
})
181+
}
182+
}
183+
142184
func Test_GetMe_IFC_FeatureFlag(t *testing.T) {
143185
t.Parallel()
144186

pkg/github/helper_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,16 @@ func createMCPRequest(args any) mcp.CallToolRequest {
323323
}
324324
}
325325

326+
// createMCPRequestWithRawArguments creates a CallToolRequest with the given raw JSON arguments.
327+
// Use nil or an empty slice to simulate clients that omit the arguments field.
328+
func createMCPRequestWithRawArguments(args json.RawMessage) mcp.CallToolRequest {
329+
return mcp.CallToolRequest{
330+
Params: &mcp.CallToolParamsRaw{
331+
Arguments: args,
332+
},
333+
}
334+
}
335+
326336
// Well-known MCP client names used in tests.
327337
const (
328338
ClientNameVSCodeInsiders = "Visual Studio Code - Insiders"

pkg/inventory/server_tool.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ func NewServerToolWithContextHandler[In any, Out any](tool mcp.Tool, toolset Too
133133
HandlerFunc: func(_ any) mcp.ToolHandler {
134134
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
135135
var arguments In
136-
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
136+
args := req.Params.Arguments
137+
// Some MCP clients omit arguments for zero-parameter tools; treat as {}.
138+
if len(args) == 0 {
139+
args = json.RawMessage(`{}`)
140+
}
141+
if err := json.Unmarshal(args, &arguments); err != nil {
137142
return &mcp.CallToolResult{
138143
Content: []mcp.Content{
139144
&mcp.TextContent{Text: fmt.Sprintf("invalid arguments: %s", err)},

pkg/inventory/server_tool_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,93 @@ func TestNewServerToolWithContextHandler_ValidArguments_Succeeds(t *testing.T) {
7878
require.True(t, ok)
7979
assert.Equal(t, "success: octocat/hello-world", textContent.Text)
8080
}
81+
82+
func TestNewServerToolWithContextHandler_EmptyArguments_TreatedAsEmptyObject(t *testing.T) {
83+
tool := NewServerToolWithContextHandler(
84+
mcp.Tool{Name: "zero_arg_tool"},
85+
testToolsetMetadata("test"),
86+
func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
87+
if len(args) != 0 {
88+
return &mcp.CallToolResult{
89+
Content: []mcp.Content{
90+
&mcp.TextContent{Text: "expected empty arguments"},
91+
},
92+
IsError: true,
93+
}, nil, nil
94+
}
95+
return &mcp.CallToolResult{
96+
Content: []mcp.Content{
97+
&mcp.TextContent{Text: "ok"},
98+
},
99+
}, args, nil
100+
},
101+
)
102+
103+
handler := tool.HandlerFunc(nil)
104+
105+
tests := []struct {
106+
name string
107+
arguments json.RawMessage
108+
}{
109+
{name: "nil arguments", arguments: nil},
110+
{name: "empty byte slice", arguments: json.RawMessage{}},
111+
{name: "explicit empty object", arguments: json.RawMessage(`{}`)},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
result, err := handler(context.Background(), &mcp.CallToolRequest{
117+
Params: &mcp.CallToolParamsRaw{
118+
Name: "zero_arg_tool",
119+
Arguments: tt.arguments,
120+
},
121+
})
122+
123+
require.NoError(t, err)
124+
require.NotNil(t, result)
125+
assert.False(t, result.IsError)
126+
})
127+
}
128+
}
129+
130+
func TestNewServerToolWithContextHandler_EmptyArguments_TypedEmptyStruct(t *testing.T) {
131+
type emptyArgs struct{}
132+
133+
tool := NewServerToolWithContextHandler(
134+
mcp.Tool{Name: "typed_zero_arg_tool"},
135+
testToolsetMetadata("test"),
136+
func(_ context.Context, _ *mcp.CallToolRequest, args emptyArgs) (*mcp.CallToolResult, any, error) {
137+
return &mcp.CallToolResult{
138+
Content: []mcp.Content{
139+
&mcp.TextContent{Text: "ok"},
140+
},
141+
}, args, nil
142+
},
143+
)
144+
145+
handler := tool.HandlerFunc(nil)
146+
147+
tests := []struct {
148+
name string
149+
arguments json.RawMessage
150+
}{
151+
{name: "nil arguments", arguments: nil},
152+
{name: "empty byte slice", arguments: json.RawMessage{}},
153+
{name: "explicit empty object", arguments: json.RawMessage(`{}`)},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
result, err := handler(context.Background(), &mcp.CallToolRequest{
159+
Params: &mcp.CallToolParamsRaw{
160+
Name: "typed_zero_arg_tool",
161+
Arguments: tt.arguments,
162+
},
163+
})
164+
165+
require.NoError(t, err)
166+
require.NotNil(t, result)
167+
assert.False(t, result.IsError)
168+
})
169+
}
170+
}

0 commit comments

Comments
 (0)