Class: ActionMCP::Tool

Inherits:
Capability show all
Extended by:
SchemaHelpers
Includes:
Callbacks, CurrentHelpers
Defined in:
lib/action_mcp/tool.rb

Overview

Base class for defining tools.

Provides a DSL for specifying metadata, properties, and nested collection schemas. Tools are registered automatically in the ToolsRegistry unless marked as abstract.

Direct Known Subclasses

ApplicationMCPTool

Instance Attribute Summary collapse

Attributes inherited from Capability

#execution_context

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Capability

abstract!, abstract?, capability_name, description, #session, #with_context

Constructor Details

#initialize(attributes = {}) ⇒ Tool

Override initialize to validate parameters before ActiveModel conversion



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/action_mcp/tool.rb', line 336

def initialize(attributes = {})
  # Separate additional properties from defined attributes if enabled
  if self.class.accepts_additional_properties?
    defined_keys = self.class.schema_property_keys
    # Use partition for single-pass separation - more efficient than except/slice
    defined_attrs, additional_attrs = attributes.partition { |k, _|
      defined_keys.include?(k.to_s)
    }.map(&:to_h)
    @_additional_params = additional_attrs
    attributes = defined_attrs
  else
    @_additional_params = {}
  end

  # Validate parameters before ActiveModel processes them
  validate_parameter_types(attributes)
  super
end

Instance Attribute Details

#_required_propertiesArray<String>

Returns The required properties of the tool.

Returns:

  • (Array<String>)

    The required properties of the tool.



23
# File 'lib/action_mcp/tool.rb', line 23

class_attribute :_schema_properties, instance_accessor: false, default: {}

#_schema_propertiesHash

Returns The schema properties of the tool.

Returns:

  • (Hash)

    The schema properties of the tool.



23
# File 'lib/action_mcp/tool.rb', line 23

class_attribute :_schema_properties, instance_accessor: false, default: {}

Class Method Details

.accepts_additional_properties?Boolean

Returns whether this tool accepts additional properties

Returns:

  • (Boolean)


193
194
195
# File 'lib/action_mcp/tool.rb', line 193

def accepts_additional_properties?
  !_additional_properties.nil? && _additional_properties != false
end

.additional_properties(enabled = nil) ⇒ Object

Sets or retrieves the additionalProperties setting for the input schema

Parameters:

  • enabled (Boolean, Hash) (defaults to: nil)

    true to allow any additional properties, false to disallow them, or a Hash for typed additional properties



184
185
186
187
188
189
190
# File 'lib/action_mcp/tool.rb', line 184

def additional_properties(enabled = nil)
  if enabled.nil?
    _additional_properties
  else
    self._additional_properties = enabled
  end
end

.annotate(key, value) ⇒ Object



77
78
79
# File 'lib/action_mcp/tool.rb', line 77

def annotate(key, value)
  self._annotations = _annotations.merge(key.to_s => value)
end

.annotations_for_protocol(_protocol_version = nil) ⇒ Object

Return annotations for the tool



107
108
109
110
# File 'lib/action_mcp/tool.rb', line 107

def annotations_for_protocol(_protocol_version = nil)
  # Always include annotations now that we only support 2025+
  _annotations
end

.call(arguments = {}) ⇒ Object

Class method to call the tool with arguments



113
114
115
# File 'lib/action_mcp/tool.rb', line 113

def call(arguments = {})
  new(arguments).call
end

.collection(prop_name, type:, description: nil, required: false, default: []) ⇒ void

This method returns an undefined value.


Collection DSL


Defines a collection property for the tool.

Parameters:

  • prop_name (Symbol, String)

    The collection property name.

  • type (String)

    The type for collection items.

  • description (String, nil) (defaults to: nil)

    Optional description for the collection.

  • required (Boolean) (defaults to: false)

    Whether the collection is required (default: false).

  • default (Array, nil) (defaults to: [])

    The default value for the collection.

Raises:

  • (ArgumentError)


267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/action_mcp/tool.rb', line 267

def self.collection(prop_name, type:, description: nil, required: false, default: [])
  raise ArgumentError, "Type is required for a collection" if type.nil?

  collection_definition = { type: "array", items: { type: type } }
  collection_definition[:description] = description if description && !description.empty?

  self._schema_properties = _schema_properties.merge(prop_name.to_s => collection_definition)
  self._required_properties = _required_properties.dup.tap do |req|
    req << prop_name.to_s if required
  end

  # Map the type - for number arrays, use our custom type instance
  mapped_type = if type == "number"
                  Types::FloatArrayType.new
  else
                  map_json_type_to_active_model_type("array_#{type}")
  end

  attribute prop_name, mapped_type, default: default

  # For arrays, we need to check if the attribute is nil, not if it's empty
  return unless required

  validates prop_name, presence: true, unless: -> { send(prop_name).is_a?(Array) }
  validate do
    errors.add(prop_name, "can't be blank") if send(prop_name).nil?
  end
end

.default_tool_nameString Also known as: default_capability_name

Returns a default tool name based on the class name.

Returns:

  • (String)

    The default tool name.



51
52
53
54
55
# File 'lib/action_mcp/tool.rb', line 51

def self.default_tool_name
  return "" if name.nil?

  name.demodulize.underscore.sub(/_tool$/, "")
end

.destructive(enabled = true) ⇒ Object



90
91
92
# File 'lib/action_mcp/tool.rb', line 90

def destructive(enabled = true)
  annotate(:destructiveHint, enabled)
end

.destructive?Boolean

Returns:

  • (Boolean)


126
127
128
# File 'lib/action_mcp/tool.rb', line 126

def destructive?
  _annotations["destructiveHint"] == true
end

.idempotent(enabled = true) ⇒ Object



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

def idempotent(enabled = true)
  annotate(:idempotentHint, enabled)
end

.idempotent?Boolean

Returns:

  • (Boolean)


122
123
124
# File 'lib/action_mcp/tool.rb', line 122

def idempotent?
  _annotations["idempotentHint"] == true
end

.inherited(subclass) ⇒ Object

Hook called when a class inherits from Tool



69
70
71
72
73
74
75
# File 'lib/action_mcp/tool.rb', line 69

def inherited(subclass)
  super
  # Run the ActiveSupport load hook when a tool is defined
  subclass.class_eval do
    ActiveSupport.run_load_hooks(:action_mcp_tool, subclass)
  end
end

.meta(data = nil) ⇒ Object

Sets or retrieves the _meta field



161
162
163
164
165
166
167
168
169
# File 'lib/action_mcp/tool.rb', line 161

def meta(data = nil)
  if data
    raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)

    self._meta = _meta.merge(data)
  else
    _meta
  end
end

.open_world(enabled = true) ⇒ Object



102
103
104
# File 'lib/action_mcp/tool.rb', line 102

def open_world(enabled = true)
  annotate(:openWorldHint, enabled)
end

.open_world?Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/action_mcp/tool.rb', line 130

def open_world?
  _annotations["openWorldHint"] == true
end

.output_schema(&block) ⇒ Hash

Schema DSL for output structure

Parameters:

  • block (Proc)

    Block containing output schema definition

Returns:

  • (Hash)

    The generated JSON Schema



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/action_mcp/tool.rb', line 138

def output_schema(&block)
  return _output_schema unless block_given?

  builder = OutputSchemaBuilder.new
  builder.instance_eval(&block)

  # Store both the builder and the generated schema
  self._output_schema_builder = builder
  self._output_schema = builder.to_json_schema

  _output_schema
end

.output_schema_legacy(schema = nil) ⇒ Object

Legacy output_schema method for backward compatibility



152
153
154
155
156
157
158
# File 'lib/action_mcp/tool.rb', line 152

def output_schema_legacy(schema = nil)
  if schema
    raise NotImplementedError, "Legacy output schema not yet implemented. Use output_schema DSL instead!"
  end

  _output_schema
end

.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts) ⇒ void

This method returns an undefined value.


Property DSL (Direct Declaration)


Defines a property for the tool.

This method builds a JSON Schema definition for the property, registers it in the tool’s schema, and creates an ActiveModel attribute for it.

Parameters:

  • prop_name (Symbol, String)

    The property name.

  • type (String) (defaults to: "string")

    The JSON Schema type (default: “string”).

  • description (String, nil) (defaults to: nil)

    Optional description for the property.

  • required (Boolean) (defaults to: false)

    Whether the property is required (default: false).

  • default (Object, nil) (defaults to: nil)

    The default value for the property.

  • opts (Hash)

    Additional options for the JSON Schema.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/action_mcp/tool.rb', line 236

def self.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts)
  # Build the JSON Schema definition.
  prop_definition = { type: type }
  prop_definition[:description] = description if description && !description.empty?
  prop_definition.merge!(opts) if opts.any?

  self._schema_properties = _schema_properties.merge(prop_name.to_s => prop_definition)
  self._required_properties = _required_properties.dup.tap do |req|
    req << prop_name.to_s if required
  end

  # Map the JSON Schema type to an ActiveModel attribute type.
  attribute prop_name, map_json_type_to_active_model_type(type), default: default
  validates prop_name, presence: true, if: -> { required }

  return unless %w[number integer].include?(type)

  validates prop_name, numericality: true, allow_nil: !required
end

.read_only(enabled = true) ⇒ Object



94
95
96
# File 'lib/action_mcp/tool.rb', line 94

def read_only(enabled = true)
  annotate(:readOnlyHint, enabled)
end

.read_only?Boolean

Helper methods for checking annotations

Returns:

  • (Boolean)


118
119
120
# File 'lib/action_mcp/tool.rb', line 118

def read_only?
  _annotations["readOnlyHint"] == true
end

.requires_consent!Object

Marks this tool as requiring consent before execution



172
173
174
# File 'lib/action_mcp/tool.rb', line 172

def requires_consent!
  self._requires_consent = true
end

.requires_consent?Boolean

Returns whether this tool requires consent

Returns:

  • (Boolean)


177
178
179
# File 'lib/action_mcp/tool.rb', line 177

def requires_consent?
  _requires_consent
end

.schema_property_keysObject

Returns cached string keys for schema properties to avoid repeated conversions



198
199
200
201
202
203
# File 'lib/action_mcp/tool.rb', line 198

def schema_property_keys
  return _cached_schema_property_keys if _cached_schema_property_keys

  self._cached_schema_property_keys = _schema_properties.keys.map(&:to_s)
  _cached_schema_property_keys
end

.title(value = nil) ⇒ Object

Convenience methods for common annotations



82
83
84
85
86
87
88
# File 'lib/action_mcp/tool.rb', line 82

def title(value = nil)
  if value
    annotate(:title, value)
  else
    _annotations["title"]
  end
end

.to_h(protocol_version: nil) ⇒ Hash


Tool Definition Serialization


Returns a hash representation of the tool definition including its JSON Schema.

Returns:

  • (Hash)

    The tool definition.



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/action_mcp/tool.rb', line 302

def self.to_h(protocol_version: nil)
  schema = {
    type: "object",
    properties: _schema_properties
  }
  schema[:required] = _required_properties if _required_properties.any?

  # Add additionalProperties if configured
  add_additional_properties_to_schema(schema, _additional_properties)

  result = {
    name: tool_name,
    description: description.presence,
    inputSchema: schema
  }.compact

  # Add output schema if defined
  result[:outputSchema] = _output_schema if _output_schema.present?

  # Add annotations if protocol supports them
  annotations = annotations_for_protocol(protocol_version)
  result[:annotations] = annotations if annotations.any?

  # Add _meta if present
  result[:_meta] = _meta if _meta.any?

  result
end

.tool_name(name = nil) ⇒ String


Tool Name and Description DSL


Sets or retrieves the tool’s name.

Parameters:

  • name (String, nil) (defaults to: nil)

    Optional. The name to set for the tool.

Returns:

  • (String)

    The current tool name.



40
41
42
43
44
45
46
# File 'lib/action_mcp/tool.rb', line 40

def self.tool_name(name = nil)
  if name
    self._capability_name = name
  else
    _capability_name || default_tool_name
  end
end

.typeObject



60
61
62
# File 'lib/action_mcp/tool.rb', line 60

def type
  :tool
end

.unregister_from_registryObject



64
65
66
# File 'lib/action_mcp/tool.rb', line 64

def unregister_from_registry
  ActionMCP::ToolsRegistry.unregister(self) if ActionMCP::ToolsRegistry.items.values.include?(self)
end

Instance Method Details

#additional_paramsObject

Returns additional parameters that were passed but not defined in the schema



356
357
358
# File 'lib/action_mcp/tool.rb', line 356

def additional_params
  @_additional_params || {}
end

#callObject

Public entry point for executing the tool Returns an array of Content objects collected from render calls



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/action_mcp/tool.rb', line 362

def call
  @response = ToolResponse.new
  performed = false            # ← track execution

  if valid?
    begin
      run_callbacks :perform do
        performed = true       # ← set if we reach the block
        perform
      end
    rescue StandardError => e
      # Show generic error message for HTTP requests, detailed for direct calls
      error_message = if execution_context[:request].present?
                         "An unexpected error occurred."
      else
        e.message
      end
      @response.mark_as_error!(:internal_error, message: error_message)
    end
  else
    @response.mark_as_error!(:invalid_params,
                             message: "Invalid input",
                             data: errors.full_messages)
  end

  # If callbacks halted execution (`performed` still false) and
  # nothing else marked an error, surface it as invalid_params.
  if !performed && !@response.error?
    @response.mark_as_error!(:invalid_params, message: "Tool execution was aborted")
  end

  @response
end

#inspectObject



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/action_mcp/tool.rb', line 396

def inspect
  attributes_hash = attributes.transform_values(&:inspect)

  response_info = if defined?(@response) && @response
                    "response: #{@response.contents.size} content(s), isError: #{@response.is_error}"
  else
                    "response: nil"
  end

  errors_info = errors.any? ? ", errors: #{errors.full_messages}" : ""

  "#<#{self.class.name} #{attributes_hash.map do |k, v|
    "#{k}: #{v.inspect}"
  end.join(', ')}, #{response_info}#{errors_info}>"
end

#render(structured: nil, **args) ⇒ Object

Override render to collect Content objects and support structured content



413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/action_mcp/tool.rb', line 413

def render(structured: nil, **args)
  if structured
    # Render structured content
    set_structured_content(structured)
    structured
  else
    # Normal content rendering
    content = super(**args) # Call Renderable's render method
    @response.add(content)  # Add to the response
    content # Return the content for potential use in perform
  end
end

Override render_resource_link to collect ResourceLink objects



427
428
429
430
431
# File 'lib/action_mcp/tool.rb', line 427

def render_resource_link(**args)
  content = super(**args) # Call Renderable's render_resource_link method
  @response.add(content)  # Add to the response
  content # Return the content for potential use in perform
end