diff --git a/mcp.go b/mcp.go index 2b95887..f05573d 100644 --- a/mcp.go +++ b/mcp.go @@ -21,6 +21,13 @@ type mcpTool struct { } func (t *mcpTool) Tool() openai.Tool { + // A tool without arguments has no properties, and the unmarshal leaves + // the map nil. jsonschema.Definition encoded that as {}, but this + // struct would send null, which vLLM and others reject. + props := t.props + if props == nil { + props = map[string]jsonschema.Definition{} + } return openai.Tool{ Type: openai.ToolTypeFunction, Function: &openai.FunctionDefinition{ @@ -28,7 +35,7 @@ func (t *mcpTool) Tool() openai.Tool { Description: t.description, Parameters: mcpToolParameters{ Type: jsonschema.Object, - Properties: t.props, + Properties: props, Required: t.inputSchema.Required, AdditionalProperties: t.inputSchema.AdditionalProperties, }, diff --git a/mcp_params_test.go b/mcp_params_test.go new file mode 100644 index 0000000..3f5ab13 --- /dev/null +++ b/mcp_params_test.go @@ -0,0 +1,34 @@ +package cogito + +import ( + "encoding/json" + "testing" +) + +// An MCP tool that takes no arguments has no "properties" in its input +// schema, so the unmarshal leaves props nil. OpenAI-compatible servers +// such as vLLM validate the schema and reject "properties": null, which +// fails the whole request, not only this tool. +func TestMCPToolWithoutArgumentsSendsEmptyProperties(t *testing.T) { + tool := &mcpTool{ + name: "list_things", + inputSchema: toolInputSchema{Type: "object"}, + } + + dat, err := json.Marshal(tool.Tool().Function.Parameters) + if err != nil { + t.Fatalf("marshal parameters: %v", err) + } + + var params map[string]any + if err := json.Unmarshal(dat, ¶ms); err != nil { + t.Fatalf("unmarshal parameters: %v", err) + } + props, ok := params["properties"].(map[string]any) + if !ok { + t.Fatalf("properties must be an object, got %s", dat) + } + if len(props) != 0 { + t.Fatalf("properties must be empty, got %s", dat) + } +}