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
"operator", "fulfillment"
read_only!
dynamic_contract Handlers::ListOrders
end
Defined Under Namespace
Classes: NotAuthorizedError
Class Attribute Summary collapse
-
._contract_handler ⇒ Object
readonly
: untyped.
-
._permission ⇒ Object
readonly
: Symbol?.
-
._tags ⇒ Object
readonly
: Array?.
Class Method Summary collapse
-
.authorization(permission) ⇒ Object
Declare the permission flag required to see this tool.
-
.call(server_context: nil, **params) ⇒ Object
Execute the tool by delegating to the handler.
-
.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.
-
.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
._contract_handler ⇒ Object (readonly)
: untyped
31 32 33 |
# File 'lib/mcp_authorization/tool.rb', line 31 def _contract_handler @_contract_handler end |
._permission ⇒ Object (readonly)
: Symbol?
25 26 27 |
# File 'lib/mcp_authorization/tool.rb', line 25 def @_permission end |
._tags ⇒ Object (readonly)
: Array?
28 29 30 |
# File 'lib/mcp_authorization/tool.rb', line 28 def @_tags end |
Class Method Details
.authorization(permission) ⇒ Object
Declare the permission flag required to see this tool. : (Symbol) -> void
41 42 43 |
# File 'lib/mcp_authorization/tool.rb', line 41 def () @_permission = 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
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/mcp_authorization/tool.rb', line 128 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 |
.closed_world! ⇒ Object
: () -> void
63 |
# File 'lib/mcp_authorization/tool.rb', line 63 def closed_world!; merge_annotations(open_world_hint: false) end |
.destructive! ⇒ Object
: () -> void
55 |
# File 'lib/mcp_authorization/tool.rb', line 55 def destructive!; merge_annotations(destructive_hint: true) end |
.dynamic_contract(handler_class) ⇒ Object
Point this tool at its handler class. : (untyped) -> void
67 68 69 70 |
# File 'lib/mcp_authorization/tool.rb', line 67 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
74 75 76 |
# File 'lib/mcp_authorization/tool.rb', line 74 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]
80 81 82 83 84 85 |
# File 'lib/mcp_authorization/tool.rb', line 80 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]?
89 90 91 92 93 94 |
# File 'lib/mcp_authorization/tool.rb', line 89 def dynamic_output_schema(server_context:) McpAuthorization::RbsSchemaCompiler.compile_output( _contract_handler, server_context: server_context ) end |
.idempotent! ⇒ Object
: () -> void
59 |
# File 'lib/mcp_authorization/tool.rb', line 59 def idempotent!; merge_annotations(idempotent_hint: true) end |
.inherited(subclass) ⇒ Object
: (Class) -> void
34 35 36 37 |
# File 'lib/mcp_authorization/tool.rb', line 34 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?
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/mcp_authorization/tool.rb', line 146 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
57 |
# File 'lib/mcp_authorization/tool.rb', line 57 def not_destructive!; merge_annotations(destructive_hint: false) end |
.open_world! ⇒ Object
: () -> void
61 |
# File 'lib/mcp_authorization/tool.rb', line 61 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. : (untyped) -> bool
98 99 100 101 |
# File 'lib/mcp_authorization/tool.rb', line 98 def permitted?(server_context) return true if .nil? server_context.current_user.can?() end |
.read_only! ⇒ Object
MCP annotation hint shorthands : () -> void
53 |
# File 'lib/mcp_authorization/tool.rb', line 53 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]
183 184 185 186 |
# File 'lib/mcp_authorization/tool.rb', line 183 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
47 48 49 |
# File 'lib/mcp_authorization/tool.rb', line 47 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]?
106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/mcp_authorization/tool.rb', line 106 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 |