Skip to content

Commit 859a975

Browse files
authored
Merge pull request #1696 from dgageot/gpt-5.2
Fix MCP tool calls with gpt 5.2
2 parents 6528531 + 8a6327b commit 859a975

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

pkg/tools/mcp/mcp.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ func (ts *Toolset) callTool(ctx context.Context, toolCall tools.ToolCall) (*tool
218218
return nil, fmt.Errorf("failed to parse tool arguments: %w", err)
219219
}
220220

221+
// Strip null values from arguments. Some models (e.g. OpenAI) send explicit
222+
// null for optional parameters, but MCP servers may reject them because
223+
// null is not a valid value for the declared parameter type (e.g. string).
224+
// Omitting the key is semantically equivalent to null for optional params.
225+
for k, v := range args {
226+
if v == nil {
227+
delete(args, k)
228+
}
229+
}
230+
221231
request := &mcp.CallToolParams{}
222232
request.Name = toolCall.Function.Name
223233
request.Arguments = args

pkg/tools/mcp/mcp_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"iter"
6+
"testing"
7+
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/docker/cagent/pkg/tools"
13+
)
14+
15+
// mockMCPClient is a test double for the mcpClient interface.
16+
type mockMCPClient struct {
17+
callToolFn func(ctx context.Context, request *mcp.CallToolParams) (*mcp.CallToolResult, error)
18+
}
19+
20+
func (m *mockMCPClient) Initialize(context.Context, *mcp.InitializeRequest) (*mcp.InitializeResult, error) {
21+
return &mcp.InitializeResult{}, nil
22+
}
23+
24+
func (m *mockMCPClient) ListTools(context.Context, *mcp.ListToolsParams) iter.Seq2[*mcp.Tool, error] {
25+
return func(func(*mcp.Tool, error) bool) {}
26+
}
27+
28+
func (m *mockMCPClient) CallTool(ctx context.Context, request *mcp.CallToolParams) (*mcp.CallToolResult, error) {
29+
return m.callToolFn(ctx, request)
30+
}
31+
32+
func (m *mockMCPClient) ListPrompts(context.Context, *mcp.ListPromptsParams) iter.Seq2[*mcp.Prompt, error] {
33+
return func(func(*mcp.Prompt, error) bool) {}
34+
}
35+
36+
func (m *mockMCPClient) GetPrompt(context.Context, *mcp.GetPromptParams) (*mcp.GetPromptResult, error) {
37+
return &mcp.GetPromptResult{}, nil
38+
}
39+
40+
func (m *mockMCPClient) SetElicitationHandler(tools.ElicitationHandler) {}
41+
42+
func (m *mockMCPClient) SetOAuthSuccessHandler(func()) {}
43+
44+
func (m *mockMCPClient) SetManagedOAuth(bool) {}
45+
46+
func (m *mockMCPClient) Close(context.Context) error { return nil }
47+
48+
func TestCallToolStripsNullArguments(t *testing.T) {
49+
t.Parallel()
50+
51+
tests := []struct {
52+
name string
53+
arguments string
54+
expectedArgs map[string]any
55+
}{
56+
{
57+
name: "all null values are stripped",
58+
arguments: `{"dir": null, "pattern": null}`,
59+
expectedArgs: map[string]any{},
60+
},
61+
{
62+
name: "only null values are stripped",
63+
arguments: `{"dir": ".", "pattern": null, "extra": "value"}`,
64+
expectedArgs: map[string]any{"dir": ".", "extra": "value"},
65+
},
66+
{
67+
name: "empty arguments stay empty",
68+
arguments: `{}`,
69+
expectedArgs: map[string]any{},
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
t.Parallel()
76+
77+
var capturedArgs map[string]any
78+
79+
ts := &Toolset{
80+
started: true,
81+
mcpClient: &mockMCPClient{
82+
callToolFn: func(_ context.Context, request *mcp.CallToolParams) (*mcp.CallToolResult, error) {
83+
if m, ok := request.Arguments.(map[string]any); ok {
84+
capturedArgs = m
85+
}
86+
return &mcp.CallToolResult{
87+
Content: []mcp.Content{&mcp.TextContent{Text: "ok"}},
88+
}, nil
89+
},
90+
},
91+
}
92+
93+
result, err := ts.callTool(t.Context(), tools.ToolCall{
94+
Function: tools.FunctionCall{
95+
Name: "test_tool",
96+
Arguments: tt.arguments,
97+
},
98+
})
99+
100+
require.NoError(t, err)
101+
assert.Equal(t, "ok", result.Output)
102+
assert.Equal(t, tt.expectedArgs, capturedArgs)
103+
})
104+
}
105+
}

0 commit comments

Comments
 (0)