mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd/x/chatd): prevent nil required field in MCP tool schemas for OpenAI (#23538)
This commit is contained in:
@@ -968,7 +968,11 @@ func buildToolDefinitions(tools []fantasy.AgentTool, activeTools []string, provi
|
||||
inputSchema := map[string]any{
|
||||
"type": "object",
|
||||
"properties": info.Parameters,
|
||||
"required": info.Required,
|
||||
}
|
||||
// Only include "required" when non-empty so that a nil slice
|
||||
// never serializes to null, which OpenAI rejects.
|
||||
if len(info.Required) > 0 {
|
||||
inputSchema["required"] = info.Required
|
||||
}
|
||||
schema.Normalize(inputSchema)
|
||||
prepared = append(prepared, fantasy.FunctionTool{
|
||||
|
||||
@@ -429,11 +429,17 @@ func newMCPTool(
|
||||
}
|
||||
|
||||
func (t *mcpToolWrapper) Info() fantasy.ToolInfo {
|
||||
// Ensure Required is never nil so that it serializes to [] instead
|
||||
// of null. OpenAI rejects null for the JSON Schema "required" field.
|
||||
required := t.required
|
||||
if required == nil {
|
||||
required = []string{}
|
||||
}
|
||||
return fantasy.ToolInfo{
|
||||
Name: t.prefixedName,
|
||||
Description: t.description,
|
||||
Parameters: t.parameters,
|
||||
Required: t.required,
|
||||
Required: required,
|
||||
Parallel: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,43 @@ func TestConnectAll_ToolInfoParameters(t *testing.T) {
|
||||
assert.Contains(t, info.Required, "input")
|
||||
}
|
||||
|
||||
// TestConnectAll_NilRequiredBecomesEmptySlice verifies that a tool
|
||||
// whose inputSchema omits "required" produces an empty slice instead
|
||||
// of nil. A nil slice serializes to JSON null, which OpenAI rejects
|
||||
// with "None is not of type 'array'".
|
||||
func TestConnectAll_NilRequiredBecomesEmptySlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
// noRequiredTool defines a tool with no required parameters.
|
||||
noRequiredTool := mcpserver.ServerTool{
|
||||
Tool: mcp.NewTool("optional_only",
|
||||
mcp.WithDescription("A tool with no required fields"),
|
||||
mcp.WithString("note", mcp.Description("An optional note")),
|
||||
),
|
||||
Handler: func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return mcp.NewToolResultText("ok"), nil
|
||||
},
|
||||
}
|
||||
|
||||
ts := newTestMCPServer(t, noRequiredTool)
|
||||
cfg := makeConfig("srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
info := tools[0].Info()
|
||||
// Required must be a non-nil empty slice, not nil.
|
||||
require.NotNil(t, info.Required, "Required should never be nil")
|
||||
assert.Empty(t, info.Required, "Required should be empty for tools without required fields")
|
||||
|
||||
// Verify it serializes to [] not null.
|
||||
bs, err := json.Marshal(info.Required)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "[]", string(bs))
|
||||
}
|
||||
|
||||
// TestConnectAll_APIKeyAuth verifies that api_key auth sends the
|
||||
// configured header and value on every request.
|
||||
func TestConnectAll_APIKeyAuth(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user