-
-
Notifications
You must be signed in to change notification settings - Fork 79
feat: Add allowed_tools and require_approval to option transforms
#328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+133
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | |
| # Only hash servers are supported for MCP tool extraction | |
| next unless server.is_a?(Hash) | |
| server_hash = server.deep_symbolize_keys | |
| allowed_tools = server_hash[:allowed_tools] | |
| next unless allowed_tools.present? && allowed_tools.is_a?(Array) | |
| configs_array = allowed_tools.filter_map do |tool| | |
| case tool | |
| when String, Symbol | |
| name = tool.to_s | |
| next if name.empty? | |
| [name, { enabled: true }] | |
| when Hash | |
| tool_hash = tool.respond_to?(:deep_symbolize_keys) ? tool.deep_symbolize_keys : tool | |
| name = tool_hash[:name] || tool_hash["name"] | |
| next unless name | |
| name_str = name.to_s | |
| next if name_str.empty? | |
| [name_str, { enabled: true }] | |
| else | |
| # Ignore unsupported tool formats | |
| next | |
| end | |
| end | |
| next if configs_array.empty? | |
| { | |
| type: "mcp_toolset", | |
| mcp_server_name: server_hash[:name], | |
| default_config: { | |
| enabled: false | |
| }, | |
| configs: configs_array.to_h | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" } | ||
| ] | ||
| } | ||
|
Comment on lines
+696
to
+705
|
||
| ] | ||
|
|
||
| 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 | ||
|
Comment on lines
+802
to
+806
|
||
|
|
||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
normalize_params, when:toolsis nil and MCPs are provided,params[:tools] = normalize_mcp_tools(mcps)will set:toolstonilwhen noallowed_toolsare present (becausenormalize_mcp_toolsreturnsnilvia.presence). This introduces atools: nilkeyword arg that previously wouldn’t be sent and may serialize as"tools": null/ fail Anthropic model validation. Consider only assigning:toolswhen toolsets were actually extracted (or deleting the key when the result is nil).