actionmcp 0.80.1 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_mcp/types/float_array_type"
4
+ require "action_mcp/schema_helpers"
4
5
 
5
6
  module ActionMCP
6
7
  # Base class for defining tools.
@@ -10,6 +11,7 @@ module ActionMCP
10
11
  class Tool < Capability
11
12
  include ActionMCP::Callbacks
12
13
  include ActionMCP::CurrentHelpers
14
+ extend ActionMCP::SchemaHelpers
13
15
 
14
16
  # --------------------------------------------------------------------------
15
17
  # Class Attributes for Tool Metadata and Schema
@@ -24,6 +26,9 @@ module ActionMCP
24
26
  class_attribute :_output_schema, instance_accessor: false, default: nil
25
27
  class_attribute :_meta, instance_accessor: false, default: {}
26
28
  class_attribute :_requires_consent, instance_accessor: false, default: false
29
+ class_attribute :_output_schema_builder, instance_accessor: false, default: nil
30
+ class_attribute :_additional_properties, instance_accessor: false, default: nil
31
+ class_attribute :_cached_schema_property_keys, instance_accessor: false, default: nil
27
32
 
28
33
  # --------------------------------------------------------------------------
29
34
  # Tool Name and Description DSL
@@ -126,10 +131,27 @@ module ActionMCP
126
131
  _annotations["openWorldHint"] == true
127
132
  end
128
133
 
129
- # Sets the output schema for structured content
130
- def output_schema(schema = nil)
134
+
135
+ # Schema DSL for output structure
136
+ # @param block [Proc] Block containing output schema definition
137
+ # @return [Hash] The generated JSON Schema
138
+ def output_schema(&block)
139
+ return _output_schema unless block_given?
140
+
141
+ builder = OutputSchemaBuilder.new
142
+ builder.instance_eval(&block)
143
+
144
+ # Store both the builder and the generated schema
145
+ self._output_schema_builder = builder
146
+ self._output_schema = builder.to_json_schema
147
+
148
+ _output_schema
149
+ end
150
+
151
+ # Legacy output_schema method for backward compatibility
152
+ def output_schema_legacy(schema = nil)
131
153
  if schema
132
- raise NotImplementedError, "Output schema DSL not yet implemented. Coming soon with structured content DSL!"
154
+ raise NotImplementedError, "Legacy output schema not yet implemented. Use output_schema DSL instead!"
133
155
  end
134
156
 
135
157
  _output_schema
@@ -155,6 +177,45 @@ module ActionMCP
155
177
  def requires_consent?
156
178
  _requires_consent
157
179
  end
180
+
181
+ # Sets or retrieves the additionalProperties setting for the input schema
182
+ # @param enabled [Boolean, Hash] true to allow any additional properties,
183
+ # false to disallow them, or a Hash for typed additional properties
184
+ def additional_properties(enabled = nil)
185
+ if enabled.nil?
186
+ _additional_properties
187
+ else
188
+ self._additional_properties = enabled
189
+ end
190
+ end
191
+
192
+ # Returns whether this tool accepts additional properties
193
+ def accepts_additional_properties?
194
+ !_additional_properties.nil? && _additional_properties != false
195
+ end
196
+
197
+ # Returns cached string keys for schema properties to avoid repeated conversions
198
+ def schema_property_keys
199
+ return _cached_schema_property_keys if _cached_schema_property_keys
200
+
201
+ self._cached_schema_property_keys = _schema_properties.keys.map(&:to_s)
202
+ _cached_schema_property_keys
203
+ end
204
+
205
+ # Clear cached keys when properties change - use metaprogramming to avoid duplication
206
+ [ :property, :collection ].each do |method_name|
207
+ define_method(method_name) do |prop_name, **opts|
208
+ invalidate_schema_cache
209
+ super(prop_name, **opts)
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ # Invalidate cached schema property keys
216
+ def invalidate_schema_cache
217
+ self._cached_schema_property_keys = nil
218
+ end
158
219
  end
159
220
 
160
221
  # --------------------------------------------------------------------------
@@ -245,6 +306,9 @@ module ActionMCP
245
306
  }
246
307
  schema[:required] = _required_properties if _required_properties.any?
247
308
 
309
+ # Add additionalProperties if configured
310
+ add_additional_properties_to_schema(schema, _additional_properties)
311
+
248
312
  result = {
249
313
  name: tool_name,
250
314
  description: description.presence,
@@ -270,11 +334,29 @@ module ActionMCP
270
334
 
271
335
  # Override initialize to validate parameters before ActiveModel conversion
272
336
  def initialize(attributes = {})
337
+ # Separate additional properties from defined attributes if enabled
338
+ if self.class.accepts_additional_properties?
339
+ defined_keys = self.class.schema_property_keys
340
+ # Use partition for single-pass separation - more efficient than except/slice
341
+ defined_attrs, additional_attrs = attributes.partition { |k, _|
342
+ defined_keys.include?(k.to_s)
343
+ }.map(&:to_h)
344
+ @_additional_params = additional_attrs
345
+ attributes = defined_attrs
346
+ else
347
+ @_additional_params = {}
348
+ end
349
+
273
350
  # Validate parameters before ActiveModel processes them
274
351
  validate_parameter_types(attributes)
275
352
  super
276
353
  end
277
354
 
355
+ # Returns additional parameters that were passed but not defined in the schema
356
+ def additional_params
357
+ @_additional_params || {}
358
+ end
359
+
278
360
  # Public entry point for executing the tool
279
361
  # Returns an array of Content objects collected from render calls
280
362
  def call
@@ -290,9 +372,9 @@ module ActionMCP
290
372
  rescue StandardError => e
291
373
  # Show generic error message for HTTP requests, detailed for direct calls
292
374
  error_message = if execution_context[:request].present?
293
- "An unexpected error occurred."
375
+ "An unexpected error occurred."
294
376
  else
295
- e.message
377
+ e.message
296
378
  end
297
379
  @response.mark_as_error!(:internal_error, message: error_message)
298
380
  end
@@ -327,11 +409,18 @@ module ActionMCP
327
409
  end.join(', ')}, #{response_info}#{errors_info}>"
328
410
  end
329
411
 
330
- # Override render to collect Content objects
331
- def render(**args)
332
- content = super(**args) # Call Renderable's render method
333
- @response.add(content) # Add to the response
334
- content # Return the content for potential use in perform
412
+ # Override render to collect Content objects and support structured content
413
+ def render(structured: nil, **args)
414
+ if structured
415
+ # Render structured content
416
+ set_structured_content(structured)
417
+ structured
418
+ else
419
+ # Normal content rendering
420
+ content = super(**args) # Call Renderable's render method
421
+ @response.add(content) # Add to the response
422
+ content # Return the content for potential use in perform
423
+ end
335
424
  end
336
425
 
337
426
  # Override render_resource_link to collect ResourceLink objects
@@ -352,25 +441,21 @@ module ActionMCP
352
441
  private
353
442
 
354
443
  # Helper method for tools to manually report errors
444
+ # Uses the MCP-compliant tool execution error format
355
445
  def report_error(message)
356
- @response.mark_as_error!
357
- render text: message
446
+ @response.report_tool_error(message)
358
447
  end
359
448
 
360
449
  # Helper method to set structured content
361
450
  def set_structured_content(content)
362
451
  return unless @response
363
452
 
364
- # Validate against output schema if defined
365
- # TODO: Add JSON Schema validation here
366
- # For now, just ensure it's a hash/object
367
- if self.class._output_schema && !content.is_a?(Hash)
368
- raise ArgumentError, "Structured content must be a hash/object when output_schema is defined"
369
- end
370
-
371
453
  @response.set_structured_content(content)
372
454
  end
373
455
 
456
+ private
457
+
458
+
374
459
  # Maps a JSON Schema type to an ActiveModel attribute type.
375
460
  #
376
461
  # @param type [String] The JSON Schema type.
@@ -3,7 +3,7 @@
3
3
  module ActionMCP
4
4
  # Manages the collection of content objects for tool results
5
5
  class ToolResponse < BaseResponse
6
- attr_reader :contents, :structured_content
6
+ attr_reader :contents, :structured_content, :tool_execution_error
7
7
 
8
8
  delegate :empty?, :size, :each, :find, :map, to: :contents
9
9
 
@@ -11,6 +11,7 @@ module ActionMCP
11
11
  super
12
12
  @contents = []
13
13
  @structured_content = nil
14
+ @tool_execution_error = false # Track if this is a tool execution error
14
15
  end
15
16
 
16
17
  # Add content to the response
@@ -24,6 +25,26 @@ module ActionMCP
24
25
  @structured_content = content
25
26
  end
26
27
 
28
+ # Report a tool execution error (as opposed to protocol error)
29
+ # This follows MCP spec for tool execution errors
30
+ def report_tool_error(message)
31
+ @tool_execution_error = true
32
+ add(Content::Text.new(message))
33
+ end
34
+
35
+ def to_h(_options = nil)
36
+ if @tool_execution_error
37
+ result = {
38
+ isError: true,
39
+ content: @contents.map(&:to_h)
40
+ }
41
+ result[:structuredContent] = @structured_content if @structured_content
42
+ result
43
+ else
44
+ super
45
+ end
46
+ end
47
+
27
48
  # Implementation of build_success_hash for ToolResponse
28
49
  def build_success_hash
29
50
  result = {
@@ -35,12 +56,14 @@ module ActionMCP
35
56
 
36
57
  # Implementation of compare_with_same_class for ToolResponse
37
58
  def compare_with_same_class(other)
38
- contents == other.contents && is_error == other.is_error && structured_content == other.structured_content
59
+ contents == other.contents && is_error == other.is_error &&
60
+ structured_content == other.structured_content &&
61
+ tool_execution_error == other.tool_execution_error
39
62
  end
40
63
 
41
64
  # Implementation of hash_components for ToolResponse
42
65
  def hash_components
43
- [ contents, is_error, structured_content ]
66
+ [ contents, is_error, structured_content, tool_execution_error ]
44
67
  end
45
68
 
46
69
  # Pretty print for better debugging
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.80.1"
5
+ VERSION = "0.82.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -31,7 +31,6 @@ end.setup
31
31
  module ActionMCP
32
32
  require_relative "action_mcp/version"
33
33
  require_relative "action_mcp/client"
34
- include Logging
35
34
 
36
35
  # Protocol version constants
37
36
  SUPPORTED_VERSIONS = [
@@ -32,8 +32,19 @@ class <%= class_name %> < ApplicationMCPTool
32
32
  <% end %>
33
33
  <% end %>
34
34
 
35
+ # Uncomment to allow additional properties beyond those defined above:
36
+ # additional_properties true # Allow any additional properties
37
+ # additional_properties false # Explicitly disallow additional properties
38
+ # additional_properties({"type" => "string"}) # Allow additional properties but restrict to strings
39
+
35
40
  def perform
36
41
  render(text: "Processing <%= properties.map { |p| p[:name] }.join(', ') %>")
42
+
43
+ # If additional_properties is enabled, you can access extra parameters:
44
+ # extra_params = additional_params
45
+ # extra_params.each do |key, value|
46
+ # render(text: "Additional #{key}: #{value}")
47
+ # end
37
48
 
38
49
  # Optional outputs:
39
50
  # render(audio: "<base64_data>", mime_type: "audio/mpeg")
@@ -41,6 +52,6 @@ class <%= class_name %> < ApplicationMCPTool
41
52
  # render(resource: "file://path", mime_type: "application/json", text: "{}")
42
53
  # render(resource: "file://path", mime_type: "application/octet-stream", blob: "<base64_data>")
43
54
  rescue => e
44
- render(error: ["Error: #{e.message}"])
55
+ report_error("Error: #{e.message}")
45
56
  end
46
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.80.1
4
+ version: 0.82.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -206,6 +206,12 @@ files:
206
206
  - lib/action_mcp/json_rpc_handler_base.rb
207
207
  - lib/action_mcp/log_subscriber.rb
208
208
  - lib/action_mcp/logging.rb
209
+ - lib/action_mcp/logging/level.rb
210
+ - lib/action_mcp/logging/logger.rb
211
+ - lib/action_mcp/logging/mixin.rb
212
+ - lib/action_mcp/logging/null_logger.rb
213
+ - lib/action_mcp/logging/state.rb
214
+ - lib/action_mcp/output_schema_builder.rb
209
215
  - lib/action_mcp/prompt.rb
210
216
  - lib/action_mcp/prompt_response.rb
211
217
  - lib/action_mcp/prompts_registry.rb
@@ -216,6 +222,7 @@ files:
216
222
  - lib/action_mcp/resource_response.rb
217
223
  - lib/action_mcp/resource_template.rb
218
224
  - lib/action_mcp/resource_templates_registry.rb
225
+ - lib/action_mcp/schema_helpers.rb
219
226
  - lib/action_mcp/server.rb
220
227
  - lib/action_mcp/server/active_record_session_store.rb
221
228
  - lib/action_mcp/server/base_messaging.rb
@@ -226,6 +233,7 @@ files:
226
233
  - lib/action_mcp/server/elicitation.rb
227
234
  - lib/action_mcp/server/error_aware.rb
228
235
  - lib/action_mcp/server/error_handling.rb
236
+ - lib/action_mcp/server/handlers/logging_handler.rb
229
237
  - lib/action_mcp/server/handlers/prompt_handler.rb
230
238
  - lib/action_mcp/server/handlers/resource_handler.rb
231
239
  - lib/action_mcp/server/handlers/router.rb