actionmcp 0.100.0 → 0.101.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b01884f2ba4ded4a59d6c235583edbd00dae36da78215da26a1fbf83039838f
4
- data.tar.gz: c97745755f33ece7ce05a4a2a904cde23e3fe67c3f1ecf9ee0cc962e6bdc9177
3
+ metadata.gz: 9bf1821cef23dc83ceb6798b128cecc43a2585d3bd7c75996824cbfb960f085a
4
+ data.tar.gz: fb1b755390487c5b4fff6a75e94540470ef21135bed4978465da9f16f8f201c3
5
5
  SHA512:
6
- metadata.gz: 545540e721a557860d9263873c841fd703afd1ea9bed05ea489b738c5a81594cc6f560fdf3959d6d9ff992b3e4b247eb8efead8f5f234946429903b00ce06a96
7
- data.tar.gz: 8c4a3d2abe05b4f65b26e6dfdd751350973e029341c8ed8bb1e3f880a046477afd59ce236a71d6e607dd82ab3220eaac1c3ee462c7c8861c1ec79a814eaa42ca
6
+ metadata.gz: a0764d4fea510580d99b6b51428b91545bfd17d9e6ae9828a88b127e6d91c2f5f1d77fb0816f4043ed6de596c0c55719d2b16c73efc38a7dedf5ff027a5236a4
7
+ data.tar.gz: d828f74be45e7b33877b01265a159fcc76961bba326ac4f31205243bd6bbcd5cb1f4935be627d5ae6bb8aad1fa7644f8611b2514637f94cab74eab7f6184a2d8
data/README.md CHANGED
@@ -219,6 +219,111 @@ sum_tool = CalculateSumTool.new(a: 5, b: 10)
219
219
  result = sum_tool.call
220
220
  ```
221
221
 
222
+ #### Structured output (output_schema)
223
+
224
+ Advertise a JSON Schema for your tool's structuredContent and return machine-validated results alongside any text output.
225
+
226
+ ```ruby
227
+ class PriceQuoteTool < ApplicationMCPTool
228
+ tool_name "price_quote"
229
+ description "Return a structured price quote"
230
+
231
+ property :sku, type: "string", description: "SKU to price", required: true
232
+
233
+ output_schema do
234
+ string :sku, required: true, description: "SKU that was priced"
235
+ number :price_cents, required: true, description: "Total price in cents"
236
+ object :meta do
237
+ string :currency, required: true, enum: %w[USD EUR GBP]
238
+ boolean :cached, default: false
239
+ end
240
+ end
241
+
242
+ def perform
243
+ price_cents = lookup_price_cents(sku) # Implement your lookup
244
+
245
+ render structured: { sku: sku,
246
+ price_cents: price_cents,
247
+ meta: { currency: "USD", cached: false } }
248
+ end
249
+ end
250
+ ```
251
+
252
+ The schema is included in the tool definition, and the `structured` payload is emitted as `structuredContent` in the response while remaining compatible with text/audio/image renders.
253
+
254
+ #### Returning resource links from a tool
255
+
256
+ When you want to hand back a URI instead of embedding the payload, use the built-in `render_resource_link`, which produces the MCP `resource_link` content type.
257
+
258
+ ```ruby
259
+ class ReportLinkTool < ApplicationMCPTool
260
+ tool_name "report_link"
261
+ description "Return a downloadable report link"
262
+
263
+ property :report_id, type: "string", required: true
264
+
265
+ def perform
266
+ render_resource_link(
267
+ uri: "reports://#{report_id}.json",
268
+ name: "Report #{report_id}",
269
+ description: "Downloadable JSON for report #{report_id}",
270
+ mime_type: "application/json"
271
+ )
272
+ end
273
+ end
274
+ ```
275
+
276
+ Clients can resolve the URI with a separate `resources/read` call, keeping tool responses lightweight while still discoverable.
277
+
278
+ #### Task-augmented tools (async execution with progress)
279
+
280
+ Use MCP Tasks when work might take seconds/minutes. Advertise task support with `task_required!` (or `task_optional!`) and let callers opt in by sending `_meta.task` on `tools/call`. While running as a task, you can emit progress updates with `report_progress!`.
281
+
282
+ ```ruby
283
+ class BatchIndexTool < ApplicationMCPTool
284
+ tool_name "batch_index"
285
+ description "Index many items asynchronously with progress updates"
286
+
287
+ task_required! # advertise that this tool is intended to run as a task
288
+ property :items, type: "array_string", description: "Items to index", required: true
289
+
290
+ def perform
291
+ total = items.length
292
+ items.each_with_index do |item, idx|
293
+ index_item(item) # your indexing logic
294
+
295
+ percent = ((idx + 1) * 100.0 / total).round
296
+ report_progress!(percent: percent, message: "Indexed #{idx + 1}/#{total}")
297
+ end
298
+
299
+ render(text: "Indexed #{total} items")
300
+ end
301
+
302
+ private
303
+
304
+ def index_item(item)
305
+ # Implement your indexing logic here
306
+ end
307
+ end
308
+ ```
309
+
310
+ Call it as a task from a client by adding `_meta.task` (creates a Task record and runs the tool via `ToolExecutionJob`):
311
+
312
+ ```json
313
+ {
314
+ "jsonrpc": "2.0",
315
+ "id": 1,
316
+ "method": "tools/call",
317
+ "params": {
318
+ "name": "batch_index",
319
+ "arguments": { "items": ["a", "b", "c"] },
320
+ "_meta": { "task": { "ttl": 120000, "pollInterval": 2000 } }
321
+ }
322
+ }
323
+ ```
324
+
325
+ Poll task status with `tasks/get` or fetch the result when finished with `tasks/result`. Use `tasks/cancel` to stop non-terminal tasks.
326
+
222
327
  ### ActionMCP::ResourceTemplate
223
328
 
224
329
  `ActionMCP::ResourceTemplate` facilitates the creation of URI templates for dynamic resources that LLMs can access.
@@ -311,6 +416,10 @@ ActionMCP provides comprehensive documentation across multiple specialized guide
311
416
  - Transport configuration and connection handling
312
417
  - Tool, prompt, and resource collections
313
418
  - Production deployment patterns
419
+ - **[🔐 GATEWAY.md](GATEWAY.md)** - Authentication gateway guide
420
+ - Implementing `ApplicationGateway`
421
+ - Identifier handling via `ActionMCP::Current`
422
+ - Auth patterns, error handling, and hardening tips
314
423
 
315
424
  ### Protocol & Technical Details
316
425
  - **[🚀 The Hitchhiker's Guide to MCP](The_Hitchhikers_Guide_to_MCP.md)** - Protocol versions and migration
@@ -27,7 +27,7 @@ module ActionMCP
27
27
  @session = step(:validate_session, @task)
28
28
  return unless @session
29
29
 
30
- @tool = step(:prepare_tool, @session, tool_name, arguments)
30
+ @tool = step(:prepare_tool, @session, tool_name, arguments, @task)
31
31
  return unless @tool
32
32
 
33
33
  step(:execute_tool) do
@@ -60,7 +60,7 @@ module ActionMCP
60
60
  session
61
61
  end
62
62
 
63
- def prepare_tool(session, tool_name, arguments)
63
+ def prepare_tool(session, tool_name, arguments, task)
64
64
  tool_class = session.registered_tools.find { |t| t.tool_name == tool_name }
65
65
  unless tool_class
66
66
  @task.update(status_message: "Tool '#{tool_name}' not found")
@@ -76,6 +76,8 @@ module ActionMCP
76
76
  params: @task.request_params
77
77
  }
78
78
  })
79
+ # Enable report_progress! inside the tool during task-augmented runs
80
+ tool.instance_variable_set(:@_task, task)
79
81
 
80
82
  tool
81
83
  end
@@ -47,7 +47,9 @@ module ActionMCP
47
47
  # --- Tasks Options (MCP 2025-11-25) ---
48
48
  :tasks_enabled,
49
49
  :tasks_list_enabled,
50
- :tasks_cancel_enabled
50
+ :tasks_cancel_enabled,
51
+ # --- Schema Validation Options ---
52
+ :validate_structured_content
51
53
 
52
54
  def initialize
53
55
  @logging_enabled = false
@@ -69,6 +71,9 @@ module ActionMCP
69
71
  @tasks_list_enabled = true
70
72
  @tasks_cancel_enabled = true
71
73
 
74
+ # Schema validation - disabled by default for backward compatibility
75
+ @validate_structured_content = false
76
+
72
77
  # Gateway - resolved lazily to account for Zeitwerk autoloading
73
78
  @gateway_class_name = nil
74
79
 
@@ -127,12 +127,12 @@ module ActionMCP
127
127
  end
128
128
 
129
129
  # Define an object property
130
- # @param name [Symbol] Object property name
130
+ # @param name [Symbol, nil] Object property name. If nil, returns schema directly (for array items)
131
131
  # @param required [Boolean] Whether the object is required
132
132
  # @param description [String] Property description
133
133
  # @param additional_properties [Boolean, Hash] Whether to allow additional properties
134
134
  # @param block [Proc] Block defining object properties
135
- def object(name, required: false, description: nil, additional_properties: nil, &block)
135
+ def object(name = nil, required: false, description: nil, additional_properties: nil, &block)
136
136
  raise ArgumentError, "Object definition requires a block" unless block_given?
137
137
 
138
138
  # Create nested builder for object properties
@@ -149,10 +149,14 @@ module ActionMCP
149
149
  # Add additionalProperties if specified
150
150
  add_additional_properties_to_schema(schema, additional_properties)
151
151
 
152
- @properties[name.to_s] = schema
153
- @required << name.to_s if required
154
-
155
- name.to_s
152
+ if name
153
+ @properties[name.to_s] = schema
154
+ @required << name.to_s if required
155
+ name.to_s
156
+ else
157
+ # Return schema directly for use in array items
158
+ schema
159
+ end
156
160
  end
157
161
 
158
162
  # Set additionalProperties for the root schema
@@ -490,6 +490,9 @@ module ActionMCP
490
490
  # Override render to collect Content objects and support structured content
491
491
  def render(structured: nil, **args)
492
492
  if structured
493
+ # Validate structured content against output_schema if enabled
494
+ validate_structured_content!(structured) if self.class._output_schema
495
+
493
496
  # Render structured content
494
497
  set_structured_content(structured)
495
498
  structured
@@ -553,6 +556,42 @@ module ActionMCP
553
556
  @response.set_structured_content(content)
554
557
  end
555
558
 
559
+ # Validates structured content against the declared output_schema
560
+ # Only runs if validate_structured_content is enabled in configuration
561
+ # @param content [Hash] The structured content to validate
562
+ # @raise [StructuredContentValidationError] If content doesn't match schema
563
+ def validate_structured_content!(content)
564
+ return unless ActionMCP.configuration.validate_structured_content
565
+
566
+ schema = self.class._output_schema
567
+ return unless schema.present?
568
+
569
+ # Lazy load json_schemer - only required if validation is enabled
570
+ gem "json_schemer", ">= 2.4"
571
+ require "json_schemer"
572
+
573
+ schemer = JSONSchemer.schema(schema.deep_stringify_keys)
574
+ errors = schemer.validate(deep_stringify_content(content)).to_a
575
+
576
+ return if errors.empty?
577
+
578
+ error_messages = errors.map { |e| e["error"] }.join(", ")
579
+ raise ActionMCP::StructuredContentValidationError,
580
+ "Structured content does not match output_schema: #{error_messages}"
581
+ end
582
+
583
+ # Deep stringify keys for validation (handles symbols and nested structures)
584
+ def deep_stringify_content(content)
585
+ case content
586
+ when Hash
587
+ content.transform_keys(&:to_s).transform_values { |v| deep_stringify_content(v) }
588
+ when Array
589
+ content.map { |v| deep_stringify_content(v) }
590
+ else
591
+ content
592
+ end
593
+ end
594
+
556
595
  private
557
596
 
558
597
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.100.0"
5
+ VERSION = "0.101.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -31,6 +31,9 @@ module ActionMCP
31
31
  require_relative "action_mcp/version"
32
32
  require_relative "action_mcp/client"
33
33
 
34
+ # Error raised when structured content doesn't match the declared output_schema
35
+ class StructuredContentValidationError < StandardError; end
36
+
34
37
  # Protocol version constants
35
38
  SUPPORTED_VERSIONS = [
36
39
  "2025-11-25", # The Task Master - Tasks, icons, tool naming, polling SSE
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.100.0
4
+ version: 0.101.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -125,16 +125,16 @@ dependencies:
125
125
  name: json_schemer
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - "~>"
128
+ - - ">="
129
129
  - !ruby/object:Gem::Version
130
- version: '2.0'
130
+ version: '2.4'
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - "~>"
135
+ - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: '2.0'
137
+ version: '2.4'
138
138
  description: A streamlined, production-focused toolkit for building MCP servers in
139
139
  Rails applications. Provides essential base classes, authentication gateways, and
140
140
  HTTP transport with minimal dependencies.