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



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/action_mcp/tool.rb', line 414

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)


265
266
267
# File 'lib/action_mcp/tool.rb', line 265

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



256
257
258
259
260
261
262
# File 'lib/action_mcp/tool.rb', line 256

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

.annotate(key, value) ⇒ Object



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

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



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

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



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

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)


339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/action_mcp/tool.rb', line 339

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)
  new_required = _required_properties.dup
  new_required << prop_name.to_s if required
  self._required_properties = new_required

  # 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.



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

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

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

.destructive(enabled = true) ⇒ Object



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

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

.destructive?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/action_mcp/tool.rb', line 144

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

.execution_metadataObject

Returns the execution metadata including task support



231
232
233
234
235
# File 'lib/action_mcp/tool.rb', line 231

def 
  {
    taskSupport: _task_support.to_s
  }
end

.idempotent(enabled = true) ⇒ Object



116
117
118
# File 'lib/action_mcp/tool.rb', line 116

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

.idempotent?Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/action_mcp/tool.rb', line 140

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

.inherited(subclass) ⇒ Object

Hook called when a class inherits from Tool



87
88
89
90
91
92
93
# File 'lib/action_mcp/tool.rb', line 87

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



179
180
181
182
183
184
185
186
187
# File 'lib/action_mcp/tool.rb', line 179

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



120
121
122
# File 'lib/action_mcp/tool.rb', line 120

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

.open_world?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'lib/action_mcp/tool.rb', line 148

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



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/action_mcp/tool.rb', line 156

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



170
171
172
173
174
175
176
# File 'lib/action_mcp/tool.rb', line 170

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.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/action_mcp/tool.rb', line 308

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)
  new_required = _required_properties.dup
  new_required << prop_name.to_s if required
  self._required_properties = new_required

  # 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



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

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

.read_only?Boolean

Helper methods for checking annotations

Returns:

  • (Boolean)


136
137
138
# File 'lib/action_mcp/tool.rb', line 136

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

.requires_consent!Object

Marks this tool as requiring consent before execution



190
191
192
# File 'lib/action_mcp/tool.rb', line 190

def requires_consent!
  self._requires_consent = true
end

.requires_consent?Boolean

Returns whether this tool requires consent

Returns:

  • (Boolean)


195
196
197
# File 'lib/action_mcp/tool.rb', line 195

def requires_consent?
  _requires_consent
end

.resumable_steps(&block) ⇒ void

This method returns an undefined value.


Resumable Steps DSL (ActiveJob::Continuable support)


Defines resumable execution steps for long-running tools

Parameters:

  • block (Proc)

    Block containing step definitions



243
244
245
# File 'lib/action_mcp/tool.rb', line 243

def resumable_steps(&block)
  self._resumable_steps_block = block
end

.resumable_steps_defined?Boolean

Checks if tool has resumable steps defined

Returns:

  • (Boolean)


249
250
251
# File 'lib/action_mcp/tool.rb', line 249

def resumable_steps_defined?
  _resumable_steps_block.present?
end

.schema_property_keysObject

Returns cached string keys for schema properties to avoid repeated conversions



270
271
272
273
274
275
# File 'lib/action_mcp/tool.rb', line 270

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

.task_forbidden!Object



226
227
228
# File 'lib/action_mcp/tool.rb', line 226

def task_forbidden!
  self._task_support = :forbidden
end

.task_optional!Object



222
223
224
# File 'lib/action_mcp/tool.rb', line 222

def task_optional!
  self._task_support = :optional
end

.task_required!Object

Convenience methods for task support



218
219
220
# File 'lib/action_mcp/tool.rb', line 218

def task_required!
  self._task_support = :required
end

.task_support(mode = nil) ⇒ Symbol


Task Support DSL (MCP 2025-11-25)


Sets or retrieves the task support mode for this tool

Parameters:

  • mode (Symbol, nil) (defaults to: nil)

    :required, :optional, or :forbidden (default)

Returns:

  • (Symbol)

    The current task support mode



205
206
207
208
209
210
211
212
213
214
215
# File 'lib/action_mcp/tool.rb', line 205

def task_support(mode = nil)
  if mode
    unless i[required optional forbidden].include?(mode)
      raise ArgumentError, "task_support must be :required, :optional, or :forbidden"
    end

    self._task_support = mode
  else
    _task_support
  end
end

.title(value = nil) ⇒ Object

Convenience methods for common annotations



100
101
102
103
104
105
106
# File 'lib/action_mcp/tool.rb', line 100

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.



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/action_mcp/tool.rb', line 374

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 execution metadata (MCP 2025-11-25)
  # Only include if not default (forbidden) to minimize payload
  if _task_support && _task_support != :forbidden
    result[:execution] = 
  end

  # 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.



42
43
44
45
46
47
48
49
50
# File 'lib/action_mcp/tool.rb', line 42

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

.typeObject



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

def type
  :tool
end

.unregister_from_registryObject



82
83
84
# File 'lib/action_mcp/tool.rb', line 82

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



434
435
436
# File 'lib/action_mcp/tool.rb', line 434

def additional_params
  @_additional_params || {}
end

#callObject

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



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/action_mcp/tool.rb', line 440

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



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/action_mcp/tool.rb', line 474

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



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/action_mcp/tool.rb', line 491

def render(structured: nil, **args)
  if structured
    # Validate structured content against output_schema if enabled
    validate_structured_content!(structured) if self.class._output_schema

    # 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



508
509
510
511
512
# File 'lib/action_mcp/tool.rb', line 508

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