Class: McpAuthorization::ToolRegistry
- Inherits:
-
Object
- Object
- McpAuthorization::ToolRegistry
- Defined in:
- lib/mcp_authorization/tool_registry.rb
Overview
Global registry of all McpAuthorization::Tool subclasses.
Tools self-register via the inherited hook in Tool, so there is no
manual registration step — defining a class that inherits from Tool is
enough.
The registry is the entry point for two main operations:
-
Listing —
list_toolsreturns JSON-serialisable tool definitions filtered by domain and the current user's permissions. -
Materializing —
tool_classes_forreturns concrete MCP::Tool subclasses with per-user schemas baked in, ready to be handed to an MCP::Server for request handling.
Class Method Summary collapse
-
.ensure_tools_loaded! ⇒ Object
Force-loads tool directories so tool classes self-register, then runs
config.tool_producersfor tools the host generates at runtime. -
.facade_for(domain:, name:, server_context:) ⇒ Object
A single facade by name within a faceted domain, or nil when the domain is not faceted or the name matches no non-empty group for this caller.
-
.facades_for(domain:, server_context:) ⇒ Object
Grouped facade tools for a faceted domain — one synthetic MCP::Tool per non-empty category the caller has at least one permitted tool in.
-
.find_tool(name) ⇒ Object
Look up a tool by its MCP tool name across all domains.
-
.list_tools(domain:, server_context:) ⇒ Object
Tool definitions for
tools/list, filtered by domain and permissions. -
.register(tool_class) ⇒ Object
Register a tool class.
-
.registered_tools ⇒ Object
All registered tool classes.
-
.reset! ⇒ Object
Clear the registry.
-
.tool_class_for(domain:, name:, server_context:) ⇒ Object
Concrete MCP::Tool subclass for a single named tool within a domain, or nil when the tool is unknown in that domain or the current user is not permitted to use it.
-
.tool_classes_for(domain:, server_context:) ⇒ Object
Concrete MCP::Tool subclasses with per-user schemas baked in.
-
.tools_by_domain ⇒ Object
Groups registered tools by their domain tags.
Class Method Details
.ensure_tools_loaded! ⇒ Object
Force-loads tool directories so tool classes self-register, then runs
config.tool_producers for tools the host generates at runtime.
Deliberately lazy, and the ordering here is the contract:
-
Producers run last, inside this method, so a host cannot register a generated tool ahead of the
tool_pathseager-load and suppress it. -
Producers run on a registry read, never from a boot callback. Generating tool classes means loading the code they derive from, which in a Rails app pulls in a large share of the application. Doing that from
config.to_prepareruns it during:run_prepare_callbacks, which precedes:eager_load!and:finisher_hook— andconfig.i18nis only copied ontoI18nfrom a railtie-levelafter_initialize. Application code loaded that early sees an emptyI18n.load_path, so any class resolving a translation in its class body freezes "Translation missing: ..." into validators and option lists permanently. Deferring to first read sidesteps the whole ordering question rather than asking each host to solve it. -
Producers re-run after
reset!. The Engine resets the registry on every code reload; the next read repopulates it, producers included.
@tools_loaded tracks completion, and is set only after every producer
has returned. It deliberately does NOT reuse "is @registered_tools
non-empty?" as the signal, because a non-empty registry does not mean
loading succeeded: eager_load_tool_paths! registers the file-defined
tools first, so by the time a producer raises the array is already
populated — as it also is when a producer registers 40 tools and raises
on the 41st. Guarding on that would make the failure loud exactly once
and silent forever after, leaving a permanently incomplete surface. The
contract is the opposite: a producer that raises fails every read until
it is fixed. Re-running is safe — register dedupes by identity and
eager_load_dir is a no-op on an already-loaded directory.
The reentrancy guard lets a producer call back into the registry (to inspect what is already registered, say) without recursing forever.
75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/mcp_authorization/tool_registry.rb', line 75 def ensure_tools_loaded! return if @tools_loaded return if @loading_tools @loading_tools = true begin eager_load_tool_paths! McpAuthorization.config.tool_producers.each(&:call) @tools_loaded = true ensure @loading_tools = false end end |
.facade_for(domain:, name:, server_context:) ⇒ Object
A single facade by name within a faceted domain, or nil when the domain is not faceted or the name matches no non-empty group for this caller. The facade analogue of tool_class_for — a tools/call targeting one facade should not build every facade in the domain.
150 151 152 |
# File 'lib/mcp_authorization/tool_registry.rb', line 150 def facade_for(domain:, name:, server_context:) McpAuthorization::FacadeBuilder.facade_for(domain: domain, name: name, server_context: server_context) end |
.facades_for(domain:, server_context:) ⇒ Object
Grouped facade tools for a faceted domain — one synthetic MCP::Tool per non-empty category the caller has at least one permitted tool in. Returns [] for domains not configured via facet_domain. See FacadeBuilder and docs/designs/tool-grouping-facades.md.
141 142 143 |
# File 'lib/mcp_authorization/tool_registry.rb', line 141 def facades_for(domain:, server_context:) McpAuthorization::FacadeBuilder.facades_for(domain: domain, server_context: server_context) end |
.find_tool(name) ⇒ Object
Look up a tool by its MCP tool name across all domains.
156 157 158 |
# File 'lib/mcp_authorization/tool_registry.rb', line 156 def find_tool(name) registered_tools.find { |t| t.tool_name == name } end |
.list_tools(domain:, server_context:) ⇒ Object
Tool definitions for tools/list, filtered by domain and permissions.
102 103 104 105 106 107 |
# File 'lib/mcp_authorization/tool_registry.rb', line 102 def list_tools(domain:, server_context:) candidates = tools_by_domain[domain] || [] candidates.filter_map do |tool_class| tool_class.to_mcp_definition(server_context: server_context) end end |
.register(tool_class) ⇒ Object
Register a tool class. Called automatically by Tool.inherited.
21 22 23 24 |
# File 'lib/mcp_authorization/tool_registry.rb', line 21 def register(tool_class) tools = (@registered_tools ||= []) tools << tool_class unless tools.include?(tool_class) end |
.registered_tools ⇒ Object
All registered tool classes. Triggers eager loading on first access.
Gated on "loading finished", not on "the array has entries" — see
ensure_tools_loaded! for why those must not be conflated.
31 32 33 34 35 |
# File 'lib/mcp_authorization/tool_registry.rb', line 31 def registered_tools tools = (@registered_tools ||= []) ensure_tools_loaded! unless @tools_loaded tools end |
.reset! ⇒ Object
Clear the registry. Called by the Engine's reloader on code change.
The next read reloads tool_paths and re-runs tool_producers.
163 164 165 166 167 |
# File 'lib/mcp_authorization/tool_registry.rb', line 163 def reset! @registered_tools = [] @loading_tools = false @tools_loaded = false end |
.tool_class_for(domain:, name:, server_context:) ⇒ Object
Concrete MCP::Tool subclass for a single named tool within a domain, or nil when the tool is unknown in that domain or the current user is not permitted to use it.
Materializing a per-user schema is the dominant cost of handling an MCP
request, so a tools/call — which targets exactly one tool — should
compile that one tool rather than the whole domain (what
tool_classes_for does for tools/list).
128 129 130 131 132 133 134 |
# File 'lib/mcp_authorization/tool_registry.rb', line 128 def tool_class_for(domain:, name:, server_context:) tool_class = (tools_by_domain[domain] || []).find { |tc| tc.tool_name == name } return nil unless tool_class return nil unless tool_class.permitted?(server_context) tool_class.materialize_for(server_context) end |
.tool_classes_for(domain:, server_context:) ⇒ Object
Concrete MCP::Tool subclasses with per-user schemas baked in.
111 112 113 114 115 116 117 |
# File 'lib/mcp_authorization/tool_registry.rb', line 111 def tool_classes_for(domain:, server_context:) candidates = tools_by_domain[domain] || [] candidates.filter_map do |tool_class| next unless tool_class.permitted?(server_context) tool_class.materialize_for(server_context) end end |
.tools_by_domain ⇒ Object
Groups registered tools by their domain tags.
91 92 93 94 95 96 97 98 |
# File 'lib/mcp_authorization/tool_registry.rb', line 91 def tools_by_domain initial = Hash.new { |h, k| h[k] = [] } #: Hash[String, Array[singleton(McpAuthorization::Tool)]] registered_tools.each_with_object(initial) do |tool_class, map| (tool_class. || ["default"]).each do |tag| map[tag] << tool_class end end end |