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 +4 -4
- data/README.md +109 -0
- data/app/jobs/action_mcp/tool_execution_job.rb +4 -2
- data/lib/action_mcp/configuration.rb +6 -1
- data/lib/action_mcp/output_schema_builder.rb +10 -6
- data/lib/action_mcp/tool.rb +39 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +3 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bf1821cef23dc83ceb6798b128cecc43a2585d3bd7c75996824cbfb960f085a
|
|
4
|
+
data.tar.gz: fb1b755390487c5b4fff6a75e94540470ef21135bed4978465da9f16f8f201c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
data/lib/action_mcp/tool.rb
CHANGED
|
@@ -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
|
|
data/lib/action_mcp/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|