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.
- .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.
-
.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
- .idempotent(enabled = true) ⇒ Object
- .idempotent? ⇒ Boolean
-
.inherited(subclass) ⇒ Object
Hook called when a class inherits from Tool.
-
.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_schema_legacy(schema = nil) ⇒ Object
Legacy output_schema method for backward compatibility.
-
.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.
-
.requires_consent! ⇒ Object
Marks this tool as requiring consent before execution.
-
.requires_consent? ⇒ Boolean
Returns whether this tool requires consent.
-
.schema_property_keys ⇒ Object
Returns cached string keys for schema properties to avoid repeated conversions.
-
.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
Override initialize to validate parameters before ActiveModel conversion.
- #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, 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_properties ⇒ Array<String>
Returns The required properties of the tool.
23 |
# File 'lib/action_mcp/tool.rb', line 23 class_attribute :_schema_properties, instance_accessor: false, default: {} |
#_schema_properties ⇒ Hash
Returns 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
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
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.
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_name ⇒ String Also known as: default_capability_name
Returns a default tool name based on the class 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
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
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 (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
102 103 104 |
# File 'lib/action_mcp/tool.rb', line 102 def open_world(enabled = true) annotate(:openWorldHint, enabled) end |
.open_world? ⇒ 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
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.
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
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 self. = true end |
.requires_consent? ⇒ Boolean
Returns whether this tool requires consent
177 178 179 |
# File 'lib/action_mcp/tool.rb', line 177 def end |
.schema_property_keys ⇒ Object
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.
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] = if .any? result end |
.tool_name(name = nil) ⇒ String
Tool Name and Description DSL
Sets or retrieves the tool’s 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 |
.type ⇒ Object
60 61 62 |
# File 'lib/action_mcp/tool.rb', line 60 def type :tool end |
.unregister_from_registry ⇒ Object
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_params ⇒ Object
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 |
#call ⇒ Object
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 = if execution_context[:request].present? "An unexpected error occurred." else e. end @response.mark_as_error!(:internal_error, message: ) end else @response.mark_as_error!(:invalid_params, message: "Invalid input", data: errors.) 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 |
#inspect ⇒ Object
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.}" : "" "#<#{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 |
#render_resource_link(**args) ⇒ Object
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 |