Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/tools/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ func (m *MCPToolManager) loadServerTools(ctx context.Context, serverName string,
return fmt.Errorf("conv mcp tool input schema fail(unmarshal): %w, tool name: %s", err, mcpTool.Name)
}

// Fix for issue #89: Ensure object schemas have a properties field
// OpenAI function calling requires object schemas to have a "properties" field
// even if it's empty, otherwise it throws "object schema missing properties" error
if inputSchema.Type == "object" && inputSchema.Properties == nil {
inputSchema.Properties = make(openapi3.Schemas)
}

// Create prefixed tool name
prefixedName := fmt.Sprintf("%s__%s", serverName, mcpTool.Name)

Expand Down
85 changes: 84 additions & 1 deletion internal/tools/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"testing"
"time"

"github.com/cloudwego/eino/schema"
"github.com/getkin/kin-openapi/openapi3"
"github.com/mark3labs/mcphost/internal/config"
)

Expand Down Expand Up @@ -76,6 +78,87 @@ func TestMCPToolManager_LoadTools_GracefulFailure(t *testing.T) {
t.Logf("LoadTools failed gracefully with error: %v", err)
}

// TestMCPToolManager_ToolWithoutProperties tests handling of tools with no input properties
func TestMCPToolManager_ToolWithoutProperties(t *testing.T) {
manager := NewMCPToolManager()

// Create a config with a builtin todo server (which has tools with properties)
// and test the schema conversion logic
cfg := &config.Config{
MCPServers: map[string]config.MCPServerConfig{
"todo-server": {
Type: "builtin",
Name: "todo",
},
},
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// Load the tools - this should work fine
err := manager.LoadTools(ctx, cfg)
if err != nil {
t.Fatalf("Failed to load tools: %v", err)
}

// Get the loaded tools
tools := manager.GetTools()
if len(tools) == 0 {
t.Fatal("No tools were loaded")
}

// Test that we can get tool info for each tool
for _, tool := range tools {
info, err := tool.Info(ctx)
if err != nil {
t.Errorf("Failed to get tool info: %v", err)
continue
}

// Check that the tool has a valid schema
if info.ParamsOneOf == nil {
t.Errorf("Tool %s has nil ParamsOneOf", info.Name)
}

t.Logf("Tool: %s, Description: %s", info.Name, info.Desc)
}
}

// TestIssue89_ObjectSchemaMissingProperties tests the fix for issue #89
// This is a regression test for the "object schema missing properties" error
// that occurs when tools have no input parameters and use OpenAI function calling
func TestIssue89_ObjectSchemaMissingProperties(t *testing.T) {
// Create a schema that would cause the OpenAI validation error
// This simulates what might happen with tools that have no input properties
brokenSchema := &openapi3.Schema{
Type: "object",
// Properties is nil - this causes "object schema missing properties" error in OpenAI
}

// Verify the problematic state
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
t.Log("Found object schema with nil properties - this causes OpenAI validation error")
}

// Apply the fix from issue #89
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
brokenSchema.Properties = make(openapi3.Schemas)
}

// Verify the fix worked
if brokenSchema.Type == "object" && brokenSchema.Properties == nil {
t.Error("Fix failed: object schema still has nil properties")
}

// Test that we can create a ParamsOneOf from the fixed schema
// This is what would fail before the fix
paramsOneOf := schema.NewParamsOneOfByOpenAPIV3(brokenSchema)
if paramsOneOf == nil {
t.Error("Failed to create ParamsOneOf from fixed schema - OpenAI function calling would fail")
}
}

// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
Expand All @@ -84,4 +167,4 @@ func contains(s, substr string) bool {
}
}
return false
}
}