diff --git a/lib/active_agent/providers/anthropic/transforms.rb b/lib/active_agent/providers/anthropic/transforms.rb index 747ed2e0..5ad1e2cc 100644 --- a/lib/active_agent/providers/anthropic/transforms.rb +++ b/lib/active_agent/providers/anthropic/transforms.rb @@ -32,10 +32,18 @@ def normalize_params(params) params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format) - if params[:mcps] - params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps)) - elsif params[:mcp_servers] - params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) + if params[:mcps] || params[:mcp_servers] + mcps = if params[:mcps] + params.delete(:mcps) + else + params[:mcp_servers] + end + + params[:mcp_servers] = normalize_mcp_servers(mcps) + + if params[:tools].nil? # If tools not already provided, extract from mcps + params[:tools] = normalize_mcp_tools(mcps) + end end params @@ -105,6 +113,29 @@ def normalize_mcp_servers(mcp_servers) end end + def normalize_mcp_tools(mcp_servers) + return [] unless mcp_servers.is_a?(Array) + + result = mcp_servers.map do |server| + server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server + + if server_hash[:allowed_tools].present? + { + type: "mcp_toolset", + mcp_server_name: server_hash[:name], + default_config: { + enabled: false + }, + configs: server_hash[:allowed_tools].to_h { |tool| + [ tool[:name], { enabled: true } ] + } + } + end + end + + result.compact.presence + end + # Normalizes tool_choice from common format to Anthropic gem model objects. # # The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto, diff --git a/lib/active_agent/providers/open_ai/responses/transforms.rb b/lib/active_agent/providers/open_ai/responses/transforms.rb index fca00a57..0457c18c 100644 --- a/lib/active_agent/providers/open_ai/responses/transforms.rb +++ b/lib/active_agent/providers/open_ai/responses/transforms.rb @@ -96,6 +96,14 @@ def normalize_mcp_servers(mcp_servers) server_url: server_hash[:url] || server_hash[:server_url] } + if server_hash[:require_approval] + result[:require_approval] = server_hash[:require_approval] + end + + if server_hash[:allowed_tools] + result[:allowed_tools] = server_hash[:allowed_tools] + end + # Keep authorization field (OpenAI uses 'authorization', not 'authorization_token') if server_hash[:authorization] result[:authorization] = server_hash[:authorization] diff --git a/test/providers/anthropic/transforms_test.rb b/test/providers/anthropic/transforms_test.rb index 3c223a7b..5a252fb5 100644 --- a/test/providers/anthropic/transforms_test.rb +++ b/test/providers/anthropic/transforms_test.rb @@ -691,6 +691,196 @@ def transforms assert_equal "not an array", result end + + # normalize_mcp_tools tests + test "normalize_mcp_tools converts allowed_tools to mcp_toolset format" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [ + { name: "create_payment" }, + { name: "get_payment" } + ] + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_equal 1, result.size + assert_equal "mcp_toolset", result[0][:type] + assert_equal "stripe", result[0][:mcp_server_name] + assert_equal false, result[0][:default_config][:enabled] + assert_equal 2, result[0][:configs].size + assert_equal true, result[0][:configs]["create_payment"][:enabled] + assert_equal true, result[0][:configs]["get_payment"][:enabled] + end + + test "normalize_mcp_tools handles multiple servers with allowed_tools" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [ + { name: "create_payment" } + ] + }, + { + name: "github", + url: "https://api.github.com", + allowed_tools: [ + { name: "search_repos" }, + { name: "create_issue" } + ] + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_equal 2, result.size + assert_equal "stripe", result[0][:mcp_server_name] + assert_equal 1, result[0][:configs].size + assert_equal "github", result[1][:mcp_server_name] + assert_equal 2, result[1][:configs].size + end + + test "normalize_mcp_tools skips servers without allowed_tools" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [ + { name: "create_payment" } + ] + }, + { + name: "public", + url: "https://public.api.com" + # No allowed_tools + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_equal 1, result.size + assert_equal "stripe", result[0][:mcp_server_name] + end + + test "normalize_mcp_tools handles empty allowed_tools array" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [] + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_nil result + end + + test "normalize_mcp_tools returns nil for servers all without allowed_tools" do + mcp_servers = [ + { + name: "public", + url: "https://public.api.com" + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_nil result + end + + test "normalize_mcp_tools returns empty array for nil input" do + result = transforms.normalize_mcp_tools(nil) + + assert_equal [], result + end + + test "normalize_mcp_tools returns empty array for empty array" do + result = transforms.normalize_mcp_tools([]) + + assert_nil result + end + + test "normalize_mcp_tools returns empty array for non-array input" do + result = transforms.normalize_mcp_tools("not an array") + + assert_equal [], result + end + + test "normalize_mcp_tools handles single tool" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [ + { name: "create_payment" } + ] + } + ] + + result = transforms.normalize_mcp_tools(mcp_servers) + + assert_equal 1, result.size + assert_equal 1, result[0][:configs].size + assert_equal true, result[0][:configs]["create_payment"][:enabled] + end + + # Integration test for normalize_params with allowed_tools + test "normalize_params extracts mcp_tools from mcps with allowed_tools" do + params = { + mcps: [ + { + name: "stripe", + url: "https://mcp.stripe.com", + authorization: "sk_test_123", + allowed_tools: [ + { name: "create_payment" }, + { name: "get_payment" } + ] + } + ] + } + + result = transforms.normalize_params(params) + + # Should have mcp_servers + assert_equal 1, result[:mcp_servers].size + assert_equal "stripe", result[:mcp_servers][0][:name] + + # Should have extracted tools from allowed_tools + assert result[:tools].present? + assert_equal 1, result[:tools].size + assert_equal "mcp_toolset", result[:tools][0][:type] + assert_equal "stripe", result[:tools][0][:mcp_server_name] + assert_equal 2, result[:tools][0][:configs].size + end + + test "normalize_params does not override existing tools when extracting from mcps" do + params = { + tools: [ + { name: "existing_tool", input_schema: { type: "object" } } + ], + mcps: [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: [ + { name: "create_payment" } + ] + } + ] + } + + result = transforms.normalize_params(params) + + # Should keep existing tools, not override with mcp tools + assert_equal 1, result[:tools].size + assert_equal "existing_tool", result[:tools][0][:name] + end end end end diff --git a/test/providers/open_ai/responses/transforms_test.rb b/test/providers/open_ai/responses/transforms_test.rb index fe3d5df0..31b69e84 100644 --- a/test/providers/open_ai/responses/transforms_test.rb +++ b/test/providers/open_ai/responses/transforms_test.rb @@ -460,6 +460,163 @@ def serializable.serialize assert_equal [], result end + + # require_approval tests + test "normalize_mcp_servers handles require_approval always" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com", require_approval: "always" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "always", result[0][:require_approval] + end + + test "normalize_mcp_servers handles require_approval never" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com", require_approval: "never" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "never", result[0][:require_approval] + end + + test "normalize_mcp_servers handles require_approval with hash" do + servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + require_approval: { + always: ["payment_methods"], + never: ["read_operations"] + } + } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal ["payment_methods"], result[0][:require_approval][:always] + assert_equal ["read_operations"], result[0][:require_approval][:never] + end + + test "normalize_mcp_servers without require_approval omits field" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_nil result[0][:require_approval] + end + + # allowed_tools tests + test "normalize_mcp_servers handles allowed_tools array" do + servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + allowed_tools: ["create_payment", "get_payment"] + } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal ["create_payment", "get_payment"], result[0][:allowed_tools] + end + + test "normalize_mcp_servers handles empty allowed_tools array" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com", allowed_tools: [] } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal [], result[0][:allowed_tools] + end + + test "normalize_mcp_servers without allowed_tools omits field" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_nil result[0][:allowed_tools] + end + + # Combined require_approval and allowed_tools tests + test "normalize_mcp_servers handles both require_approval and allowed_tools" do + servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + authorization: "sk_test_123", + require_approval: "always", + allowed_tools: ["create_payment", "get_payment"] + } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "mcp", result[0][:type] + assert_equal "stripe", result[0][:server_label] + assert_equal "https://mcp.stripe.com", result[0][:server_url] + assert_equal "sk_test_123", result[0][:authorization] + assert_equal "always", result[0][:require_approval] + assert_equal ["create_payment", "get_payment"], result[0][:allowed_tools] + end + + test "normalize_mcp_servers handles different require_approval for multiple servers" do + servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + require_approval: "always", + allowed_tools: ["create_payment"] + }, + { + name: "github", + url: "https://api.githubcopilot.com/mcp/", + require_approval: "never", + allowed_tools: ["search_repos", "create_issue"] + } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 2, result.size + assert_equal "always", result[0][:require_approval] + assert_equal ["create_payment"], result[0][:allowed_tools] + assert_equal "never", result[1][:require_approval] + assert_equal ["search_repos", "create_issue"], result[1][:allowed_tools] + end + + test "normalize_mcp_servers preserves require_approval and allowed_tools in already normalized format" do + servers = [ + { + type: "mcp", + server_label: "stripe", + server_url: "https://mcp.stripe.com", + require_approval: "always", + allowed_tools: ["create_payment"] + } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal servers, result + assert_equal "always", result[0][:require_approval] + assert_equal ["create_payment"], result[0][:allowed_tools] + end end end end