Class: ActionMCP::Tool
- Inherits:
-
Capability
- Object
- Capability
- ActionMCP::Tool
- 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
Instance Attribute Summary collapse
-
#_required_properties ⇒ Array<String>
The required properties of the tool.
-
#_schema_properties ⇒ Hash
The schema properties of the tool.
Attributes inherited from Capability
Class Method Summary collapse
-
.accepts_additional_properties? ⇒ Boolean
Returns whether this tool accepts additional properties.
-
.additional_properties(enabled = nil) ⇒ Object
Sets or retrieves the additionalProperties setting for the input schema.
-
.alias_property(alias_name, property_name) ⇒ Object
Creates an alternate input name for an existing property.
- .annotate(key, value) ⇒ Object
-
.annotations_for_protocol(_protocol_version = nil) ⇒ Object
Return annotations for the tool.
-
.call(arguments = {}) ⇒ Object
Class method to call the tool with arguments.
- .canonical_property_name(name) ⇒ Object
-
.collection(prop_name, type:, description: nil, required: false, default: []) ⇒ void
-------------------------------------------------------------------------- Collection DSL -------------------------------------------------------------------------- Defines a collection property for the tool.
-
.default_tool_name ⇒ String
(also: default_capability_name)
Returns a default tool name based on the class name.
- .destructive(enabled = true) ⇒ Object
- .destructive? ⇒ Boolean
-
.execution_metadata ⇒ Object
Returns the execution metadata including task support.
-
.from_wire(arguments) ⇒ Object
Builds a tool from decoded MCP arguments.
- .idempotent(enabled = true) ⇒ Object
- .idempotent? ⇒ Boolean
-
.inherited(subclass) ⇒ Object
Hook called when a class inherits from Tool.
- .input_schema ⇒ Object
- .input_schemer ⇒ Object
-
.meta(data = nil) ⇒ Object
Sets or retrieves the _meta field.
- .open_world(enabled = true) ⇒ Object
- .open_world? ⇒ Boolean
-
.output_schema(&block) ⇒ Hash
Schema DSL for output structure.
- .output_schemer ⇒ Object
-
.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts) ⇒ void
-------------------------------------------------------------------------- Property DSL (Direct Declaration) -------------------------------------------------------------------------- Defines a property for the tool.
- .read_only(enabled = true) ⇒ Object
-
.read_only? ⇒ Boolean
Helper methods for checking annotations.
-
.renders_ui(resource_uri, visibility: nil) ⇒ Object
Declares the UI resource this tool renders.
-
.requires_consent! ⇒ Object
Marks this tool as requiring consent before execution.
-
.requires_consent? ⇒ Boolean
Returns whether this tool requires consent.
-
.resumable_steps(&block) ⇒ void
-------------------------------------------------------------------------- Resumable Steps DSL (ActiveJob::Continuable support) -------------------------------------------------------------------------- Defines resumable execution steps for long-running tools.
-
.resumable_steps_defined? ⇒ Boolean
Checks if tool has resumable steps defined.
-
.schema_property_keys ⇒ Object
Returns cached string keys for schema properties to avoid repeated conversions.
- .task_forbidden! ⇒ Object
- .task_optional! ⇒ Object
-
.task_required! ⇒ Object
Convenience methods for task support.
-
.task_support(mode = nil) ⇒ Symbol
-------------------------------------------------------------------------- Task Support DSL (MCP 2025-11-25) -------------------------------------------------------------------------- Sets or retrieves the task support mode for this tool.
-
.title(value = nil) ⇒ Object
Convenience methods for common annotations.
-
.to_h(protocol_version: nil) ⇒ Hash
-------------------------------------------------------------------------- Tool Definition Serialization -------------------------------------------------------------------------- Returns a hash representation of the tool definition including its JSON Schema.
-
.tool_name(name = nil) ⇒ String
-------------------------------------------------------------------------- Tool Name and Description DSL -------------------------------------------------------------------------- Sets or retrieves the tool's name.
- .type ⇒ Object
- .unregister_from_registry ⇒ Object
Instance Method Summary collapse
-
#additional_params ⇒ Object
Returns additional parameters that were passed but not defined in the schema.
-
#call ⇒ Object
Public entry point for executing the tool Returns an array of Content objects collected from render calls.
-
#initialize(attributes = {}) ⇒ Tool
constructor
-------------------------------------------------------------------------- Instance Methods --------------------------------------------------------------------------.
- #inspect ⇒ Object
-
#render(structured: nil, **args) ⇒ Object
Override render to collect Content objects and support structured content.
-
#render_resource_link(**args) ⇒ Object
Override render_resource_link to collect ResourceLink objects.
Methods inherited from Capability
abstract!, abstract?, capability_name, #client_supports_ui?, description, #session, #with_context
Constructor Details
#initialize(attributes = {}) ⇒ Tool
Instance Methods
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 |
# File 'lib/action_mcp/tool.rb', line 526 def initialize(attributes = {}) @_alias_conflicts = [] defined_attributes = {} additional_attributes = {} if attributes.is_a?(Hash) sources = {} attributes.each do |key, value| key_string = key.to_s property_name = self.class.canonical_property_name(key_string) if self.class._schema_properties.key?(property_name) if defined_attributes.key?(property_name) && sources[property_name] != key_string && defined_attributes[property_name] != value @_alias_conflicts << "conflicting values for '#{property_name}' via '#{sources[property_name]}' and '#{key_string}'" next end defined_attributes[property_name] = value sources[property_name] = key_string else additional_attributes[key] = value end end end @_provided_additional_attributes = additional_attributes @_additional_params = self.class.accepts_additional_properties? ? additional_attributes : {} super(defined_attributes) end |
Instance Attribute Details
#_required_properties ⇒ Array<String>
Returns The required properties of the tool.
25 |
# File 'lib/action_mcp/tool.rb', line 25 class_attribute :_schema_properties, instance_accessor: false, default: {} |
#_schema_properties ⇒ Hash
Returns The schema properties of the tool.
25 |
# File 'lib/action_mcp/tool.rb', line 25 class_attribute :_schema_properties, instance_accessor: false, default: {} |
Class Method Details
.accepts_additional_properties? ⇒ Boolean
Returns whether this tool accepts additional properties
303 304 305 |
# File 'lib/action_mcp/tool.rb', line 303 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
289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/action_mcp/tool.rb', line 289 def additional_properties(enabled = nil) if enabled.nil? _additional_properties else unless enabled == true || enabled == false || enabled.is_a?(Hash) raise ArgumentError, "additional_properties must be true, false, or a JSON Schema hash" end self._additional_properties = enabled invalidate_schema_cache end end |
.alias_property(alias_name, property_name) ⇒ Object
Creates an alternate input name for an existing property.
Both root_id and thread_id are accepted when initializing the tool,
and both readers resolve to the same ActiveModel attribute.
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 |
# File 'lib/action_mcp/tool.rb', line 363 def alias_property(alias_name, property_name) alias_key = alias_name.to_s property_key = canonical_property_name(property_name) unless _schema_properties.key?(property_key) raise ArgumentError, "Cannot alias unknown property '#{property_name}'" end if alias_key == property_key raise ArgumentError, "Cannot alias property '#{property_key}' to itself" end if _schema_properties.key?(alias_key) raise ArgumentError, "Cannot alias '#{alias_key}' because it is already defined as a property" end existing_alias = _property_aliases[alias_key] if existing_alias && existing_alias != property_key raise ArgumentError, "Alias '#{alias_key}' already points to property '#{existing_alias}'" end self._property_aliases = _property_aliases.merge(alias_key => property_key) alias_attribute alias_key, property_key invalidate_schema_cache end |
.annotate(key, value) ⇒ Object
102 103 104 |
# File 'lib/action_mcp/tool.rb', line 102 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
132 133 134 135 |
# File 'lib/action_mcp/tool.rb', line 132 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
138 139 140 |
# File 'lib/action_mcp/tool.rb', line 138 def call(arguments = {}) from_wire(arguments).call end |
.canonical_property_name(name) ⇒ Object
389 390 391 |
# File 'lib/action_mcp/tool.rb', line 389 def canonical_property_name(name) _property_aliases[name.to_s] || name.to_s 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.
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/action_mcp/tool.rb', line 449 def self.collection(prop_name, type:, description: nil, required: false, default: []) raise ArgumentError, "Type is required for a collection" if type.nil? if _property_aliases.key?(prop_name.to_s) raise ArgumentError, "Cannot define collection '#{prop_name}' because it is already defined as an alias" end invalidate_schema_cache 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 end |
.default_tool_name ⇒ String Also known as: default_capability_name
Returns a default tool name based on the class name.
76 77 78 79 80 |
# File 'lib/action_mcp/tool.rb', line 76 def self.default_tool_name return "" if name.nil? name.underscore.gsub("/", "__").sub(/_tool$/, "") end |
.destructive(enabled = true) ⇒ Object
115 116 117 |
# File 'lib/action_mcp/tool.rb', line 115 def destructive(enabled = true) annotate(:destructiveHint, enabled) end |
.destructive? ⇒ Boolean
159 160 161 |
# File 'lib/action_mcp/tool.rb', line 159 def destructive? _annotations["destructiveHint"] == true end |
.execution_metadata ⇒ Object
Returns the execution metadata including task support
264 265 266 267 268 |
# File 'lib/action_mcp/tool.rb', line 264 def { taskSupport: _task_support.to_s } end |
.from_wire(arguments) ⇒ Object
Builds a tool from decoded MCP arguments. The untouched input is kept for JSON Schema validation before ActiveModel coercion.
144 145 146 147 148 |
# File 'lib/action_mcp/tool.rb', line 144 def from_wire(arguments) new(arguments).tap do |tool| tool.instance_variable_set(:@_wire_arguments, arguments) end end |
.idempotent(enabled = true) ⇒ Object
123 124 125 |
# File 'lib/action_mcp/tool.rb', line 123 def idempotent(enabled = true) annotate(:idempotentHint, enabled) end |
.idempotent? ⇒ Boolean
155 156 157 |
# File 'lib/action_mcp/tool.rb', line 155 def idempotent? _annotations["idempotentHint"] == true end |
.inherited(subclass) ⇒ Object
Hook called when a class inherits from Tool
94 95 96 97 98 99 100 |
# File 'lib/action_mcp/tool.rb', line 94 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 |
.input_schema ⇒ Object
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/action_mcp/tool.rb', line 315 def input_schema properties = _schema_properties.deep_dup _property_aliases.each do |alias_name, property_name| properties[alias_name] = _schema_properties.fetch(property_name).deep_dup end schema = { "$schema" => SchemaValidator::DEFAULT_DIALECT, type: "object", properties: properties } required = [] alias_constraints = [] _required_properties.each do |property_name| aliases = _property_aliases.select { |_alias_name, target| target == property_name }.keys if aliases.empty? required << property_name else alternatives = [ property_name, *aliases ].map { |name| { required: [ name ] } } alias_constraints << { anyOf: alternatives } end end schema[:required] = required if required.any? schema[:allOf] = alias_constraints if alias_constraints.any? add_additional_properties_to_schema(schema, _additional_properties) schema end |
.input_schemer ⇒ Object
345 346 347 |
# File 'lib/action_mcp/tool.rb', line 345 def input_schemer self._input_schemer ||= SchemaValidator.compile(input_schema, context: "input schema for #{tool_name}") end |
.meta(data = nil) ⇒ Object
Sets or retrieves the _meta field
187 188 189 190 191 192 193 194 195 |
# File 'lib/action_mcp/tool.rb', line 187 def (data = nil) if data raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash) self. = .merge(data) else end end |
.open_world(enabled = true) ⇒ Object
127 128 129 |
# File 'lib/action_mcp/tool.rb', line 127 def open_world(enabled = true) annotate(:openWorldHint, enabled) end |
.open_world? ⇒ Boolean
163 164 165 |
# File 'lib/action_mcp/tool.rb', line 163 def open_world? _annotations["openWorldHint"] == true end |
.output_schema(&block) ⇒ Hash
Schema DSL for output structure
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/action_mcp/tool.rb', line 171 def output_schema(&block) return _output_schema unless block_given? builder = OutputSchemaBuilder.new builder.instance_eval(&block) schema = builder.to_json_schema schemer = SchemaValidator.compile(schema, context: "output schema for #{tool_name}") self._output_schema_builder = builder self._output_schema = schema self._output_schemer = schemer _output_schema end |
.output_schemer ⇒ Object
349 350 351 352 353 |
# File 'lib/action_mcp/tool.rb', line 349 def output_schemer return unless _output_schema self._output_schemer ||= SchemaValidator.compile(_output_schema, context: "output schema for #{tool_name}") 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.
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 |
# File 'lib/action_mcp/tool.rb', line 417 def self.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts) if _property_aliases.key?(prop_name.to_s) raise ArgumentError, "Cannot define property '#{prop_name}' because it is already defined as an alias" end invalidate_schema_cache # 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 end |
.read_only(enabled = true) ⇒ Object
119 120 121 |
# File 'lib/action_mcp/tool.rb', line 119 def read_only(enabled = true) annotate(:readOnlyHint, enabled) end |
.read_only? ⇒ Boolean
Helper methods for checking annotations
151 152 153 |
# File 'lib/action_mcp/tool.rb', line 151 def read_only? _annotations["readOnlyHint"] == true end |
.renders_ui(resource_uri, visibility: nil) ⇒ Object
Declares the UI resource this tool renders. Merges a ui: entry into
_meta so the tool listing advertises the dashboard.
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/action_mcp/tool.rb', line 202 def renders_ui(resource_uri, visibility: nil) unless resource_uri.is_a?(String) && Apps::URI_SCHEME.match?(resource_uri) raise ArgumentError, "renders_ui requires a ui:// URI, got: #{resource_uri.inspect}" end normalized_visibility = if visibility normalized = Array(visibility).map(&:to_s) invalid = normalized - Apps::VISIBILITY_VALUES if invalid.any? raise ArgumentError, "renders_ui visibility must be #{Apps::VISIBILITY_VALUES.join('/')}, got: #{visibility.inspect}" end normalized end = { resourceUri: resource_uri, visibility: normalized_visibility }.compact self. = .deep_merge(ui: ) end |
.requires_consent! ⇒ Object
Marks this tool as requiring consent before execution
223 224 225 |
# File 'lib/action_mcp/tool.rb', line 223 def self. = true end |
.requires_consent? ⇒ Boolean
Returns whether this tool requires consent
228 229 230 |
# File 'lib/action_mcp/tool.rb', line 228 def 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
276 277 278 |
# File 'lib/action_mcp/tool.rb', line 276 def resumable_steps(&block) self._resumable_steps_block = block end |
.resumable_steps_defined? ⇒ Boolean
Checks if tool has resumable steps defined
282 283 284 |
# File 'lib/action_mcp/tool.rb', line 282 def resumable_steps_defined? _resumable_steps_block.present? end |
.schema_property_keys ⇒ Object
Returns cached string keys for schema properties to avoid repeated conversions
308 309 310 311 312 313 |
# File 'lib/action_mcp/tool.rb', line 308 def schema_property_keys return _cached_schema_property_keys if _cached_schema_property_keys self._cached_schema_property_keys = (_schema_properties.keys + _property_aliases.keys).map(&:to_s) _cached_schema_property_keys end |
.task_forbidden! ⇒ Object
259 260 261 |
# File 'lib/action_mcp/tool.rb', line 259 def task_forbidden! self._task_support = :forbidden end |
.task_optional! ⇒ Object
255 256 257 |
# File 'lib/action_mcp/tool.rb', line 255 def task_optional! self._task_support = :optional end |
.task_required! ⇒ Object
Convenience methods for task support
251 252 253 |
# File 'lib/action_mcp/tool.rb', line 251 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
238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/action_mcp/tool.rb', line 238 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
107 108 109 110 111 112 113 |
# File 'lib/action_mcp/tool.rb', line 107 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.
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/action_mcp/tool.rb', line 482 def self.to_h(protocol_version: nil) schema = input_schema input_schemer 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 if .any? = .deep_dup = [:ui] || ["ui"] if .is_a?(Hash) resource_uri_key = :resourceUri if .key?(:resourceUri) resource_uri_key ||= "resourceUri" if .key?("resourceUri") if resource_uri_key [resource_uri_key] = Apps::ViewManifest.resolve_resource_uri([resource_uri_key]) end end result[:_meta] = end result end |
.tool_name(name = nil) ⇒ String
Tool Name and Description DSL
Sets or retrieves the tool's name.
49 50 51 52 53 54 55 56 57 |
# File 'lib/action_mcp/tool.rb', line 49 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 |
.type ⇒ Object
85 86 87 |
# File 'lib/action_mcp/tool.rb', line 85 def type :tool end |
.unregister_from_registry ⇒ Object
89 90 91 |
# File 'lib/action_mcp/tool.rb', line 89 def unregister_from_registry ActionMCP::ToolsRegistry.unregister(self) if ActionMCP::ToolsRegistry.items.values.include?(self) end |
Instance Method Details
#additional_params ⇒ Object
Returns additional parameters that were passed but not defined in the schema
560 561 562 |
# File 'lib/action_mcp/tool.rb', line 560 def additional_params @_additional_params || {} end |
#call ⇒ Object
Public entry point for executing the tool Returns an array of Content objects collected from render calls
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 |
# File 'lib/action_mcp/tool.rb', line 566 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 = if execution_context[:request].present? "An unexpected error occurred." else e. end @response.report_tool_error() end else @response.report_tool_error("Invalid input: #{errors..join(', ')}") end # If callbacks halted execution (`performed` still false) and # nothing else marked an error, surface it as a tool execution error. if !performed && !@response.error? @response.report_tool_error("Tool execution was aborted") end if performed && !@response.error? && self.class._output_schema && @response.structured_content.nil? @response.report_tool_error("Tool declared an output schema but returned no structured content") end @response end |
#inspect ⇒ Object
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 |
# File 'lib/action_mcp/tool.rb', line 602 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.}" : "" "#<#{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
619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'lib/action_mcp/tool.rb', line 619 def render(structured: nil, **args) unless structured.nil? validate_structured_content!(structured) if self.class._output_schema 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 |
#render_resource_link(**args) ⇒ Object
Override render_resource_link to collect ResourceLink objects
634 635 636 637 638 |
# File 'lib/action_mcp/tool.rb', line 634 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 |