Class: McpAuthorization::Tool

Inherits:
MCP::Tool
  • Object
show all
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"
  authorization :view_orders
  tags "operator", "fulfillment"
  read_only!

  dynamic_contract Handlers::ListOrders
end

Defined Under Namespace

Classes: NotAuthorizedError

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

._contract_handlerObject (readonly)

: untyped



31
32
33
# File 'lib/mcp_authorization/tool.rb', line 31

def _contract_handler
  @_contract_handler
end

._permissionObject (readonly)

: Symbol?



25
26
27
# File 'lib/mcp_authorization/tool.rb', line 25

def _permission
  @_permission
end

._tagsObject (readonly)

: Array?



28
29
30
# File 'lib/mcp_authorization/tool.rb', line 28

def _tags
  @_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 authorization(permission)
  @_permission = 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

Raises:



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

Returns:

  • (Boolean)


98
99
100
101
# File 'lib/mcp_authorization/tool.rb', line 98

def permitted?(server_context)
  return true if _permission.nil?
  server_context.current_user.can?(_permission)
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 tags(*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