mcp_authorization v0.7.1

16. Group a large domain into facades

A domain has grown to a hundred-plus tools. tools/list is dominated by call-time argument schemas for tools the model will never pick, and choosing from a fl...

Problem. A domain has grown to a hundred-plus tools. tools/list is dominated by call-time argument schemas for tools the model will never pick, and choosing from a flat list that long is genuinely hard.

Solution. Give each tool a category, then opt the domain into grouping. One facade per group replaces the flat list. Opt-in per domain โ€” a domain with no facet_domain is untouched, and category is inert there.

class ListOrdersTool < McpAuthorization::Tool
  tags "admin"
  category :orders                    # the group this tool belongs to
  dynamic_contract OrderHandlers::List
end
# config/initializers/mcp_authorization.rb
config.facet_domain :admin, group_by: :category

config.categories do
  summary :orders,  "Create, inspect, and update orders and their line items."
  summary :billing, "Invoices, payments, refunds, and billing profiles."
end

Calling a facade names the inner tool and its arguments:

{ "name": "orders_tools",
  "arguments": { "tool_name": "update_order", "arguments": { "id": "o_1" } } }

Result. tools/list returns orders_tools and billing_tools instead of a hundred entries. Each facade's description is routing signal only: the group summary, then one line per tool this caller may invoke โ€” so a description that varies by role varies here too. Per-tool argument schemas ride on the facade's _meta (default :vendor_extension) or are omitted entirely (:lazy).

Dispatch goes through the real call path: the name must be one advertised to this caller, the tool is re-resolved through permitted?, and the target's own filter_input/filter_output run โ€” so grouping changes the shape of the listing, never the authorization. Groups where the caller may invoke nothing are hidden entirely, so a facade never advertises an empty enum.

Tools with no category land in an uncategorized group. Pass uncategorized: :error when you'd rather CI catch the omission. Full option table and error list in the README.

Collected from COOKBOOK.md in the repository. Edit it there, not here.