Class: McpAuthorization::Tool
- Inherits:
-
MCP::Tool
- Object
- MCP::Tool
- McpAuthorization::Tool
- Defined in:
- lib/mcp_authorization/tool.rb
Overview
Base class for MCP tools with schema-shaping authorization.
Subclass this instead of MCP::Tool directly. Each subclass is a thin
declarative wrapper — the actual business logic lives in a handler
class (a plain Ruby class that includes DSL) pointed to by
dynamic_contract.
Defining a tool
class Tools::ListOrders < McpAuthorization::Tool
tool_name "list_orders"
:view_orders # RBAC permission (legacy, still supported)
gate :feature, :order_tracking # generic predicate gate (any predicate name)
"operator", "fulfillment"
read_only!
dynamic_contract Handlers::ListOrders
end
Both authorization and any number of gate declarations contribute to
visibility — the tool is shown only when every check passes. See
permitted? for the resolution order.
Defined Under Namespace
Classes: NotAuthorizedError
Class Attribute Summary collapse
-
._category ⇒ Object
readonly
: Symbol?.
-
._category_summary ⇒ Object
readonly
: String?.
-
._contract_handler ⇒ Object
readonly
: untyped.
-
._gates ⇒ Object
readonly
: Array[Hash[Symbol, untyped]]?.
-
._permission ⇒ Object
readonly
: Symbol?.
-
._tags ⇒ Object
readonly
: Array?.
Class Method Summary collapse
-
.authorization(permission) ⇒ Object
Declare the RBAC permission flag required to see this tool.
-
.call(server_context: nil, **params) ⇒ Object
Execute the tool by delegating to the handler.
-
.category(name, summary: nil) ⇒ Object
Declare the group this tool belongs to when its domain is faceted (see Configuration#facet_domain).
-
.closed_world! ⇒ Object
: () -> void.
-
.destructive! ⇒ Object
: () -> void.
-
.dynamic_contract(handler_class) ⇒ Object
Point this tool at its handler class.
-
.dynamic_description(server_context:) ⇒ Object
Build the tool description for this user.
-
.dynamic_input_schema(server_context:) ⇒ Object
Compile the input JSON Schema for this user.
-
.dynamic_output_schema(server_context:) ⇒ Object
Compile the output JSON Schema for this user.
-
.gate(predicate_name, value) ⇒ Object
Declare a generic predicate gate that must pass for this tool to be visible.
-
.idempotent! ⇒ Object
: () -> void.
-
.inherited(subclass) ⇒ Object
: (Class) -> void.
-
.materialize_for(server_context) ⇒ Object
Create an anonymous MCP::Tool subclass with this user's schemas baked in.
-
.not_destructive! ⇒ Object
: () -> void.
-
.open_world! ⇒ Object
: () -> void.
-
.permitted?(server_context) ⇒ Boolean
Check whether the current user is allowed to see this tool.
-
.read_only! ⇒ Object
MCP annotation hint shorthands : () -> void.
-
.symbolize_keys(hash) ⇒ Object
Normalize hash keys to symbols so projection output can be splatted into a handler's kwarg-only
#callsignature. -
.tags(*list) ⇒ Object
Declare which MCP domains this tool belongs to.
-
.to_mcp_definition(server_context:) ⇒ Object
Build the full MCP tool definition hash for
tools/list.
Class Attribute Details
._category ⇒ Object (readonly)
: Symbol?
41 42 43 |
# File 'lib/mcp_authorization/tool.rb', line 41 def _category @_category end |
._category_summary ⇒ Object (readonly)
: String?
44 45 46 |
# File 'lib/mcp_authorization/tool.rb', line 44 def _category_summary @_category_summary end |
._contract_handler ⇒ Object (readonly)
: untyped
47 48 49 |
# File 'lib/mcp_authorization/tool.rb', line 47 def _contract_handler @_contract_handler end |
._gates ⇒ Object (readonly)
: Array[Hash[Symbol, untyped]]?
38 39 40 |
# File 'lib/mcp_authorization/tool.rb', line 38 def _gates @_gates end |
._permission ⇒ Object (readonly)
: Symbol?
32 33 34 |
# File 'lib/mcp_authorization/tool.rb', line 32 def @_permission end |
._tags ⇒ Object (readonly)
: Array?
35 36 37 |
# File 'lib/mcp_authorization/tool.rb', line 35 def @_tags end |
Class Method Details
.authorization(permission) ⇒ Object
Declare the RBAC permission flag required to see this tool.
Convenience alias for gate :requires, permission. The generic
gate pipeline handles dispatch — calling
server_context.requires?(permission) when defined, otherwise
falling back to current_user.can?(permission). This mirrors the
field-level migration done in 0.3.0 (#12): @requires also went
through the generic predicate pipeline rather than carrying its
own special-cased branch.
_permission remains exposed for introspection — the value is
written there as before — but the actual gating goes through the
gate list at permitted? time, just like every other check.
: (Symbol) -> void
69 70 71 72 |
# File 'lib/mcp_authorization/tool.rb', line 69 def () @_permission = gate :requires, end |
.call(server_context: nil, **params) ⇒ Object
Execute the tool by delegating to the handler.
Inputs are filtered against the user's compiled input schema before
being passed to the handler, and outputs are filtered against the
user's compiled output schema before being returned. Fields and
variants gated by @requires that the user lacks permission for
never reach the handler (in) or cross the wire (out).
: (?server_context: untyped?, **untyped) -> untyped
204 205 206 207 208 209 210 211 212 213 |
# File 'lib/mcp_authorization/tool.rb', line 204 def call(server_context: nil, **params) raise NotAuthorizedError unless server_context && permitted?(server_context) filtered = McpAuthorization::RbsSchemaCompiler.filter_input( _contract_handler, params, server_context: server_context ) result = handler_instance(server_context).call(**symbolize_keys(filtered)) McpAuthorization::RbsSchemaCompiler.filter_output( _contract_handler, result, server_context: server_context ) end |
.category(name, summary: nil) ⇒ Object
Declare the group this tool belongs to when its domain is faceted (see Configuration#facet_domain). Ignored in flat domains.
class ListOrdersTool < McpAuthorization::Tool
"admin"
category :orders
end
The optional summary: is a convenience for single-tool groups; the
central config.categories registry wins on conflict.
: (Symbol | String, ?summary: String?) -> void
91 92 93 94 |
# File 'lib/mcp_authorization/tool.rb', line 91 def category(name, summary: nil) @_category = name.to_sym @_category_summary = summary end |
.closed_world! ⇒ Object
: () -> void
132 |
# File 'lib/mcp_authorization/tool.rb', line 132 def closed_world!; merge_annotations(open_world_hint: false) end |
.destructive! ⇒ Object
: () -> void
124 |
# File 'lib/mcp_authorization/tool.rb', line 124 def destructive!; merge_annotations(destructive_hint: true) end |
.dynamic_contract(handler_class) ⇒ Object
Point this tool at its handler class. : (untyped) -> void
136 137 138 139 |
# File 'lib/mcp_authorization/tool.rb', line 136 def dynamic_contract(handler_class) @_contract_handler = handler_class @_contract_validated = false end |
.dynamic_description(server_context:) ⇒ Object
Build the tool description for this user. : (server_context: untyped) -> String
143 144 145 |
# File 'lib/mcp_authorization/tool.rb', line 143 def dynamic_description(server_context:) handler_instance(server_context).description end |
.dynamic_input_schema(server_context:) ⇒ Object
Compile the input JSON Schema for this user. : (server_context: untyped) -> Hash[Symbol, untyped]
149 150 151 152 153 154 |
# File 'lib/mcp_authorization/tool.rb', line 149 def dynamic_input_schema(server_context:) McpAuthorization::RbsSchemaCompiler.compile_input( _contract_handler, server_context: server_context ) end |
.dynamic_output_schema(server_context:) ⇒ Object
Compile the output JSON Schema for this user. : (server_context: untyped) -> Hash[Symbol, untyped]?
158 159 160 161 162 163 |
# File 'lib/mcp_authorization/tool.rb', line 158 def dynamic_output_schema(server_context:) McpAuthorization::RbsSchemaCompiler.compile_output( _contract_handler, server_context: server_context ) end |
.gate(predicate_name, value) ⇒ Object
Declare a generic predicate gate that must pass for this tool to be
visible. The gate calls server_context.{predicate}?(value) at
request time. If the predicate returns false, the tool is hidden
from tools/list and rejected from tools/call.
Mirrors the field-level @predicate(:value) system: any predicate
name works, as long as the server_context implements
{predicate}?(value).
class BulkSendSmsTool < McpAuthorization::Tool
:communications # RBAC (existing)
gate :feature, :sms # hide tool unless account has SMS configured
gate :requires, :super_user # extra RBAC check beyond authorization
end
Multiple gate calls AND together — every gate must pass.
: (Symbol, untyped) -> void
116 117 118 |
# File 'lib/mcp_authorization/tool.rb', line 116 def gate(predicate_name, value) (@_gates ||= []) << { name: predicate_name.to_sym, value: value } end |
.idempotent! ⇒ Object
: () -> void
128 |
# File 'lib/mcp_authorization/tool.rb', line 128 def idempotent!; merge_annotations(idempotent_hint: true) end |
.inherited(subclass) ⇒ Object
: (Class) -> void
50 51 52 53 |
# File 'lib/mcp_authorization/tool.rb', line 50 def inherited(subclass) super McpAuthorization::ToolRegistry.register(subclass) end |
.materialize_for(server_context) ⇒ Object
Create an anonymous MCP::Tool subclass with this user's schemas baked in.
The materialized call enforces the compiled schema at runtime:
input params are stripped of unknown or permission-gated fields
before reaching the handler, and the handler's return value is
projected onto the user's output schema before being serialized.
: (untyped) -> Class?
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/mcp_authorization/tool.rb', line 222 def materialize_for(server_context) defn = to_mcp_definition(server_context: server_context) return nil unless defn handler = _contract_handler ctx = server_context symbolize = method(:symbolize_keys) Class.new(MCP::Tool) do tool_name defn[:name] description defn[:description] input_schema defn[:inputSchema] output_schema defn[:outputSchema] if defn[:outputSchema] annotations(**defn[:annotations]) if defn[:annotations]&.any? define_singleton_method(:call) do |server_context: nil, **params| effective_ctx = server_context || ctx filtered_params = McpAuthorization::RbsSchemaCompiler.filter_input( handler, params, server_context: effective_ctx ) raw = handler.new(server_context: effective_ctx).call(**symbolize.call(filtered_params)) result = McpAuthorization::RbsSchemaCompiler.filter_output( handler, raw, server_context: effective_ctx ) response_args = [{ type: "text", text: result.to_json }] if defn[:outputSchema] MCP::Tool::Response.new(response_args, structured_content: result) else MCP::Tool::Response.new(response_args) end end end end |
.not_destructive! ⇒ Object
: () -> void
126 |
# File 'lib/mcp_authorization/tool.rb', line 126 def not_destructive!; merge_annotations(destructive_hint: false) end |
.open_world! ⇒ Object
: () -> void
130 |
# File 'lib/mcp_authorization/tool.rb', line 130 def open_world!; merge_annotations(open_world_hint: true) end |
.permitted?(server_context) ⇒ Boolean
Check whether the current user is allowed to see this tool.
Evaluates every declared gate against the server context. A tool is permitted only when every gate passes. With no gates declared, the tool is unconditionally visible.
authorization :perm contributes a gate :requires, :perm
internally, so it goes through the same pipeline as every other
predicate. There is one code path for gating, not two.
: (untyped) -> bool
175 176 177 |
# File 'lib/mcp_authorization/tool.rb', line 175 def permitted?(server_context) gates_pass?(server_context) end |
.read_only! ⇒ Object
MCP annotation hint shorthands : () -> void
122 |
# File 'lib/mcp_authorization/tool.rb', line 122 def read_only!; merge_annotations(read_only_hint: true) end |
.symbolize_keys(hash) ⇒ Object
Normalize hash keys to symbols so projection output can be splatted
into a handler's kwarg-only #call signature.
: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
259 260 261 262 |
# File 'lib/mcp_authorization/tool.rb', line 259 def symbolize_keys(hash) return {} unless hash.is_a?(Hash) hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } end |
.tags(*list) ⇒ Object
Declare which MCP domains this tool belongs to. : (*String | Array) -> void
76 77 78 |
# File 'lib/mcp_authorization/tool.rb', line 76 def (*list) @_tags = list.flatten end |
.to_mcp_definition(server_context:) ⇒ Object
Build the full MCP tool definition hash for tools/list.
Returns nil if the user is not permitted.
: (server_context: untyped) -> Hash[Symbol, untyped]?
182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/mcp_authorization/tool.rb', line 182 def to_mcp_definition(server_context:) return nil unless permitted?(server_context) validate_contract!(_contract_handler) unless @_contract_validated @_contract_validated = true { name: tool_name, description: dynamic_description(server_context: server_context), inputSchema: dynamic_input_schema(server_context: server_context), outputSchema: dynamic_output_schema(server_context: server_context), annotations: @_annotations_hash || {} } end |