actionmcp 0.107.1 → 0.109.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 +78 -15
- data/app/controllers/action_mcp/application_controller.rb +9 -3
- data/app/models/action_mcp/session/task.rb +9 -1
- data/app/models/action_mcp/session.rb +35 -3
- data/db/migrate/20250512154359_consolidated_migration.rb +90 -101
- data/lib/action_mcp/configuration.rb +22 -3
- data/lib/action_mcp/content/resource.rb +17 -2
- data/lib/action_mcp/resource.rb +16 -4
- data/lib/action_mcp/resource_template.rb +4 -2
- data/lib/action_mcp/server/elicitation.rb +105 -37
- data/lib/action_mcp/server/elicitation_request.rb +100 -0
- data/lib/action_mcp/server/handlers/prompt_handler.rb +2 -2
- data/lib/action_mcp/server/pagination.rb +106 -0
- data/lib/action_mcp/server/prompts.rb +9 -4
- data/lib/action_mcp/server/resources.rb +67 -125
- data/lib/action_mcp/server/tasks.rb +33 -13
- data/lib/action_mcp/server/tools.rb +8 -19
- data/lib/action_mcp/server/transport_handler.rb +2 -0
- data/lib/action_mcp/server/url_elicitation_request.rb +60 -0
- data/lib/action_mcp/test_helper.rb +35 -0
- data/lib/action_mcp/version.rb +1 -1
- metadata +5 -10
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +0 -9
- data/db/migrate/20250727000001_remove_oauth_support.rb +0 -59
- data/db/migrate/20251125000001_create_action_mcp_session_tasks.rb +0 -29
- data/db/migrate/20251126000001_add_continuation_state_to_action_mcp_session_tasks.rb +0 -10
- data/db/migrate/20251203000001_remove_sse_support.rb +0 -31
- data/db/migrate/20251204000001_add_progress_to_session_tasks.rb +0 -12
- data/db/migrate/20260303000001_add_session_data_to_action_mcp_sessions.rb +0 -9
- data/db/migrate/20260304000001_remove_session_resources.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbc03b1ba75f2efaf3c94ff2e664d3ca4e21013e8c55651f1fccff0b8c4e889b
|
|
4
|
+
data.tar.gz: 1576d864115954653182d05080352a28b7be2a11c8fb376f0e426503a320eb7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da5bcfd15ecc9e76b4ac6c557a84a95cf91090520ec7d11fb6c3652168c80cc1820660a8311b8b173db8bd4a1773a26cd53e83b2bae4fb6874f0b79aff489482
|
|
7
|
+
data.tar.gz: 28235fee76f77b9a7a87cca20a149231d31649a980e2d19480fde5f38543d0aa7ca61d16e7d97d89a83de9d849dda6c9e323e03cbc73dacca68572a7ba2dbaf5
|
data/README.md
CHANGED
|
@@ -330,7 +330,7 @@ Call it as a task from a client by adding `_meta.task` (creates a Task record an
|
|
|
330
330
|
}
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
-
Poll task status with `tasks/get` or fetch the result when finished with `tasks/result`. Use `tasks/cancel` to stop non-terminal tasks.
|
|
333
|
+
Poll task status with `tasks/get` or fetch the result when finished with `tasks/result`. Use `tasks/cancel` to stop non-terminal tasks. `tasks/list` returns tasks in recent-first order and always paginates (default 50 per page, or `pagination_page_size` if configured). The response includes an opaque `nextCursor` when more results are available — treat cursors as opaque tokens.
|
|
334
334
|
|
|
335
335
|
### ActionMCP::ResourceTemplate
|
|
336
336
|
|
|
@@ -491,6 +491,25 @@ end
|
|
|
491
491
|
|
|
492
492
|
For dynamic versioning, consider adding the `rails_app_version` gem.
|
|
493
493
|
|
|
494
|
+
### Pagination
|
|
495
|
+
|
|
496
|
+
All list endpoints (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`) support cursor-based pagination per the MCP specification. Pagination is **off by default** — set `pagination_page_size` to enable:
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
config.action_mcp.pagination_page_size = 10
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Or in `config/mcp.yml`:
|
|
503
|
+
|
|
504
|
+
```yaml
|
|
505
|
+
shared:
|
|
506
|
+
pagination_page_size: 10
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
When set, responses include an opaque `nextCursor` when more items are available. When `nil` (default), all items are returned in a single response. Enable only when your clients support cursor-based pagination.
|
|
510
|
+
|
|
511
|
+
`tasks/list` always paginates regardless of this setting (defaults to 50 per page, or `pagination_page_size` if configured).
|
|
512
|
+
|
|
494
513
|
### Server Instructions
|
|
495
514
|
|
|
496
515
|
Server instructions help LLMs understand **what your server is for** and **when to use it**. They describe the server's purpose and goal, not technical details like rate limits or authentication (tools are self-documented via their own descriptions).
|
|
@@ -712,10 +731,12 @@ This will create:
|
|
|
712
731
|
|
|
713
732
|
## Authentication with Gateway
|
|
714
733
|
|
|
715
|
-
ActionMCP provides a Gateway system for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
|
|
734
|
+
ActionMCP provides a Gateway system for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components. For the full gateway reference including identifier classes, session persistence, profile switching, and production hardening tips, see **[GATEWAY.md](GATEWAY.md)**.
|
|
716
735
|
|
|
717
736
|
ActionMCP uses a Gateway pattern with pluggable identifiers for authentication. You can implement custom authentication strategies using session-based auth, API keys, bearer tokens, or integrate with existing authentication systems like Warden, Devise, or external OAuth providers.
|
|
718
737
|
|
|
738
|
+
> **Note:** Auth errors return HTTP 200 with a JSON-RPC error payload (not HTTP 401). This is correct per the MCP specification — all MCP communication uses JSON-RPC over HTTP, and protocol-level errors are expressed within the JSON-RPC envelope. The `initialize` request bypasses authentication per MCP spec.
|
|
739
|
+
|
|
719
740
|
### Creating an ApplicationGateway
|
|
720
741
|
|
|
721
742
|
When you run the install generator, it creates an `ApplicationGateway` class:
|
|
@@ -986,26 +1007,68 @@ class ToolTest < ActiveSupport::TestCase
|
|
|
986
1007
|
include ActionMCP::TestHelper
|
|
987
1008
|
|
|
988
1009
|
test "CalculateSumTool returns the correct sum" do
|
|
989
|
-
|
|
990
|
-
result =
|
|
991
|
-
|
|
1010
|
+
assert_mcp_tool_findable("calculate_sum")
|
|
1011
|
+
result = execute_mcp_tool("calculate_sum", a: 5, b: 10)
|
|
1012
|
+
assert_mcp_tool_output("15.0", result)
|
|
992
1013
|
end
|
|
993
1014
|
|
|
994
1015
|
test "AnalyzeCodePrompt returns the correct analysis" do
|
|
995
|
-
|
|
996
|
-
result =
|
|
997
|
-
|
|
1016
|
+
assert_mcp_prompt_findable("analyze_code")
|
|
1017
|
+
result = execute_mcp_prompt("analyze_code", language: "Ruby", code: "def hello; puts 'Hello, world!'; end")
|
|
1018
|
+
assert_mcp_prompt_output("Analyzing Ruby code: def hello; puts 'Hello, world!'; end", result)
|
|
998
1019
|
end
|
|
999
1020
|
end
|
|
1000
1021
|
```
|
|
1001
1022
|
|
|
1002
|
-
The TestHelper provides several assertion methods:
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
- `
|
|
1006
|
-
- `
|
|
1007
|
-
- `
|
|
1008
|
-
- `
|
|
1023
|
+
The TestHelper provides several assertion and execution methods:
|
|
1024
|
+
|
|
1025
|
+
**Tools:**
|
|
1026
|
+
- `assert_mcp_tool_findable(name)` - Verifies a tool exists and is registered
|
|
1027
|
+
- `execute_mcp_tool(name, **args)` - Executes a tool with arguments and asserts success
|
|
1028
|
+
- `execute_mcp_tool_with_error(name, **args)` - Executes a tool without asserting success (for testing error cases)
|
|
1029
|
+
- `assert_mcp_tool_output(expected, response)` - Asserts tool output matches expected content
|
|
1030
|
+
|
|
1031
|
+
**Prompts:**
|
|
1032
|
+
- `assert_mcp_prompt_findable(name)` - Verifies a prompt exists and is registered
|
|
1033
|
+
- `execute_mcp_prompt(name, **args)` - Executes a prompt with arguments
|
|
1034
|
+
- `assert_mcp_prompt_output(expected, response)` - Asserts prompt output matches expected content
|
|
1035
|
+
|
|
1036
|
+
**Resource Templates:**
|
|
1037
|
+
- `assert_mcp_resource_template_findable(name)` - Verifies a resource template exists and is registered
|
|
1038
|
+
- `resolve_mcp_resource(uri)` - Resolves a resource URI and asserts success
|
|
1039
|
+
- `resolve_mcp_resource_with_error(uri)` - Resolves a resource URI without asserting success (for testing error cases)
|
|
1040
|
+
|
|
1041
|
+
**General:**
|
|
1042
|
+
- `assert_mcp_error_code(code, response)` - Asserts a specific JSON-RPC error code
|
|
1043
|
+
|
|
1044
|
+
### Testing Resource Templates
|
|
1045
|
+
|
|
1046
|
+
```ruby
|
|
1047
|
+
require "test_helper"
|
|
1048
|
+
require "action_mcp/test_helper"
|
|
1049
|
+
|
|
1050
|
+
class ProductResourceTest < ActiveSupport::TestCase
|
|
1051
|
+
include ActionMCP::TestHelper
|
|
1052
|
+
|
|
1053
|
+
test "product template is registered" do
|
|
1054
|
+
assert_mcp_resource_template_findable("products")
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
test "resolves a product resource by URI" do
|
|
1058
|
+
resp = resolve_mcp_resource("ecommerce://products/1")
|
|
1059
|
+
|
|
1060
|
+
assert resp.success?
|
|
1061
|
+
assert_not_empty resp.contents
|
|
1062
|
+
assert_equal "application/json", resp.contents.first.mime_type
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
test "returns error for nonexistent product" do
|
|
1066
|
+
resp = resolve_mcp_resource_with_error("ecommerce://products/0")
|
|
1067
|
+
|
|
1068
|
+
assert resp.is_error
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
```
|
|
1009
1072
|
|
|
1010
1073
|
## Inspecting Your MCP Server
|
|
1011
1074
|
|
|
@@ -34,6 +34,7 @@ module ActionMCP
|
|
|
34
34
|
def show
|
|
35
35
|
# MCP Streamable HTTP spec allows servers to return 405 if they don't support SSE.
|
|
36
36
|
# ActionMCP uses Tasks for async operations instead of SSE streaming.
|
|
37
|
+
response.headers["Allow"] = "POST, DELETE"
|
|
37
38
|
head :method_not_allowed
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -70,6 +71,11 @@ module ActionMCP
|
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
|
|
74
|
+
if session.nil?
|
|
75
|
+
id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
|
|
76
|
+
return render_not_found("Session not found.", id)
|
|
77
|
+
end
|
|
78
|
+
|
|
73
79
|
if session.new_record?
|
|
74
80
|
session.save!
|
|
75
81
|
response.headers[MCP_SESSION_ID_HEADER] = session.id
|
|
@@ -90,7 +96,7 @@ module ActionMCP
|
|
|
90
96
|
result = json_rpc_handler.call(jsonrpc_params)
|
|
91
97
|
process_handler_results(result, session, session_initially_missing, is_initialize_request)
|
|
92
98
|
rescue StandardError => e
|
|
93
|
-
Rails.
|
|
99
|
+
Rails.error.report(e, handled: true, severity: :error)
|
|
94
100
|
id = begin
|
|
95
101
|
jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
|
|
96
102
|
rescue StandardError
|
|
@@ -123,7 +129,7 @@ module ActionMCP
|
|
|
123
129
|
Rails.logger.info "Unified DELETE: Terminated session: #{session.id}" if ActionMCP.configuration.verbose_logging
|
|
124
130
|
head :no_content
|
|
125
131
|
rescue StandardError => e
|
|
126
|
-
Rails.
|
|
132
|
+
Rails.error.report(e, handled: true, severity: :error)
|
|
127
133
|
render_internal_server_error("Failed to terminate session.")
|
|
128
134
|
end
|
|
129
135
|
end
|
|
@@ -324,7 +330,7 @@ module ActionMCP
|
|
|
324
330
|
rescue ActionMCP::UnauthorizedError => e
|
|
325
331
|
render_unauthorized(e.message)
|
|
326
332
|
rescue StandardError => e
|
|
327
|
-
Rails.
|
|
333
|
+
Rails.error.report(e, handled: true, severity: :error)
|
|
328
334
|
render_unauthorized("Authentication system error")
|
|
329
335
|
end
|
|
330
336
|
end
|
|
@@ -68,7 +68,15 @@ module ActionMCP
|
|
|
68
68
|
# Scopes - state_machines >= 0.100.0 auto-generates .with_status(:state) scopes
|
|
69
69
|
scope :terminal, -> { with_status(:completed, :failed, :cancelled) }
|
|
70
70
|
scope :non_terminal, -> { with_status(:working, :input_required) }
|
|
71
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
71
|
+
scope :recent, -> { order(created_at: :desc, id: :desc) }
|
|
72
|
+
scope :before_recent, lambda { |task|
|
|
73
|
+
table = arel_table
|
|
74
|
+
|
|
75
|
+
where(
|
|
76
|
+
table[:created_at].lt(task.created_at)
|
|
77
|
+
.or(table[:created_at].eq(task.created_at).and(table[:id].lt(task.id)))
|
|
78
|
+
)
|
|
79
|
+
}
|
|
72
80
|
|
|
73
81
|
# State machine definition per MCP spec
|
|
74
82
|
state_machine :status, initial: :working do
|
|
@@ -172,6 +172,7 @@ module ActionMCP
|
|
|
172
172
|
def register_tool(tool_class_or_name)
|
|
173
173
|
tool_name = normalize_name(tool_class_or_name, :tool)
|
|
174
174
|
return false unless tool_exists?(tool_name)
|
|
175
|
+
return true if uses_all_tools?
|
|
175
176
|
|
|
176
177
|
self.tool_registry ||= []
|
|
177
178
|
unless self.tool_registry.include?(tool_name)
|
|
@@ -184,8 +185,13 @@ module ActionMCP
|
|
|
184
185
|
|
|
185
186
|
def unregister_tool(tool_class_or_name)
|
|
186
187
|
tool_name = normalize_name(tool_class_or_name, :tool)
|
|
187
|
-
self.tool_registry ||= []
|
|
188
188
|
|
|
189
|
+
if uses_all_tools?
|
|
190
|
+
return unless tool_exists?(tool_name)
|
|
191
|
+
expand_tool_registry!
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
self.tool_registry ||= []
|
|
189
195
|
return unless self.tool_registry.delete(tool_name)
|
|
190
196
|
|
|
191
197
|
save!
|
|
@@ -195,6 +201,7 @@ module ActionMCP
|
|
|
195
201
|
def register_prompt(prompt_class_or_name)
|
|
196
202
|
prompt_name = normalize_name(prompt_class_or_name, :prompt)
|
|
197
203
|
return false unless prompt_exists?(prompt_name)
|
|
204
|
+
return true if uses_all_prompts?
|
|
198
205
|
|
|
199
206
|
self.prompt_registry ||= []
|
|
200
207
|
unless self.prompt_registry.include?(prompt_name)
|
|
@@ -207,8 +214,13 @@ module ActionMCP
|
|
|
207
214
|
|
|
208
215
|
def unregister_prompt(prompt_class_or_name)
|
|
209
216
|
prompt_name = normalize_name(prompt_class_or_name, :prompt)
|
|
210
|
-
self.prompt_registry ||= []
|
|
211
217
|
|
|
218
|
+
if uses_all_prompts?
|
|
219
|
+
return unless prompt_exists?(prompt_name)
|
|
220
|
+
expand_prompt_registry!
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
self.prompt_registry ||= []
|
|
212
224
|
return unless self.prompt_registry.delete(prompt_name)
|
|
213
225
|
|
|
214
226
|
save!
|
|
@@ -218,6 +230,7 @@ module ActionMCP
|
|
|
218
230
|
def register_resource_template(template_class_or_name)
|
|
219
231
|
template_name = normalize_name(template_class_or_name, :resource_template)
|
|
220
232
|
return false unless resource_template_exists?(template_name)
|
|
233
|
+
return true if uses_all_resources?
|
|
221
234
|
|
|
222
235
|
self.resource_registry ||= []
|
|
223
236
|
unless self.resource_registry.include?(template_name)
|
|
@@ -230,8 +243,13 @@ module ActionMCP
|
|
|
230
243
|
|
|
231
244
|
def unregister_resource_template(template_class_or_name)
|
|
232
245
|
template_name = normalize_name(template_class_or_name, :resource_template)
|
|
233
|
-
self.resource_registry ||= []
|
|
234
246
|
|
|
247
|
+
if uses_all_resources?
|
|
248
|
+
return unless resource_template_exists?(template_name)
|
|
249
|
+
expand_resource_registry!
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
self.resource_registry ||= []
|
|
235
253
|
return unless self.resource_registry.delete(template_name)
|
|
236
254
|
|
|
237
255
|
save!
|
|
@@ -348,6 +366,20 @@ module ActionMCP
|
|
|
348
366
|
self.resource_registry = [ "*" ]
|
|
349
367
|
end
|
|
350
368
|
|
|
369
|
+
# Expand wildcard registries to explicit name lists so individual
|
|
370
|
+
# entries can be removed without breaking the wildcard check.
|
|
371
|
+
def expand_tool_registry!
|
|
372
|
+
self.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def expand_prompt_registry!
|
|
376
|
+
self.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def expand_resource_registry!
|
|
380
|
+
self.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
|
|
381
|
+
end
|
|
382
|
+
|
|
351
383
|
def normalize_name(class_or_name, type)
|
|
352
384
|
case class_or_name
|
|
353
385
|
when String
|
|
@@ -1,113 +1,102 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class ConsolidatedMigration < ActiveRecord::Migration[8.
|
|
4
|
-
def
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
t.json :resource_registry, default: []
|
|
23
|
-
t.timestamps
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Create session messages table
|
|
28
|
-
unless table_exists?(:action_mcp_session_messages)
|
|
29
|
-
create_table :action_mcp_session_messages do |t|
|
|
30
|
-
t.references :session, null: false,
|
|
31
|
-
foreign_key: { to_table: :action_mcp_sessions,
|
|
32
|
-
on_delete: :cascade,
|
|
33
|
-
on_update: :cascade,
|
|
34
|
-
name: 'fk_action_mcp_session_messages_session_id' }, type: :string
|
|
35
|
-
t.string :direction, null: false, comment: 'The message recipient', default: 'client'
|
|
36
|
-
t.string :message_type, null: false, comment: 'The type of the message'
|
|
37
|
-
t.string :jsonrpc_id
|
|
38
|
-
t.json :message_json
|
|
39
|
-
t.boolean :is_ping, default: false, null: false, comment: 'Whether the message is a ping'
|
|
40
|
-
t.boolean :request_acknowledged, default: false, null: false
|
|
41
|
-
t.boolean :request_cancelled, null: false, default: false
|
|
42
|
-
t.timestamps
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Create session subscriptions table
|
|
47
|
-
unless table_exists?(:action_mcp_session_subscriptions)
|
|
48
|
-
create_table :action_mcp_session_subscriptions do |t|
|
|
49
|
-
t.references :session,
|
|
50
|
-
null: false,
|
|
51
|
-
foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
|
|
52
|
-
type: :string
|
|
53
|
-
t.string :uri, null: false
|
|
54
|
-
t.datetime :last_notification_at
|
|
55
|
-
t.timestamps
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Add missing columns to existing tables if they exist
|
|
60
|
-
|
|
61
|
-
# For action_mcp_sessions
|
|
62
|
-
if table_exists?(:action_mcp_sessions)
|
|
63
|
-
unless column_exists?(:action_mcp_sessions, :tool_registry)
|
|
64
|
-
add_column :action_mcp_sessions, :tool_registry, :json, default: []
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
unless column_exists?(:action_mcp_sessions, :prompt_registry)
|
|
68
|
-
add_column :action_mcp_sessions, :prompt_registry, :json, default: []
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
unless column_exists?(:action_mcp_sessions, :resource_registry)
|
|
72
|
-
add_column :action_mcp_sessions, :resource_registry, :json, default: []
|
|
73
|
-
end
|
|
3
|
+
class ConsolidatedMigration < ActiveRecord::Migration[8.1]
|
|
4
|
+
def up
|
|
5
|
+
create_table :action_mcp_sessions, id: :string, if_not_exists: true do |t|
|
|
6
|
+
t.string :role, null: false, default: "server", comment: "The role of the session"
|
|
7
|
+
t.string :status, null: false, default: "pre_initialize"
|
|
8
|
+
t.datetime :ended_at, comment: "The time the session ended"
|
|
9
|
+
t.string :protocol_version
|
|
10
|
+
t.json :server_capabilities, comment: "The capabilities of the server"
|
|
11
|
+
t.json :client_capabilities, comment: "The capabilities of the client"
|
|
12
|
+
t.json :server_info, comment: "The information about the server"
|
|
13
|
+
t.json :client_info, comment: "The information about the client"
|
|
14
|
+
t.boolean :initialized, null: false, default: false
|
|
15
|
+
t.integer :messages_count, null: false, default: 0
|
|
16
|
+
t.json :tool_registry, default: []
|
|
17
|
+
t.json :prompt_registry, default: []
|
|
18
|
+
t.json :resource_registry, default: []
|
|
19
|
+
t.json :consents, default: {}, null: false
|
|
20
|
+
t.json :session_data, default: {}, null: false
|
|
21
|
+
t.timestamps
|
|
74
22
|
end
|
|
75
23
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
24
|
+
add_column :action_mcp_sessions, :consents, :json, default: {}, null: false unless column_exists?(:action_mcp_sessions, :consents)
|
|
25
|
+
add_column :action_mcp_sessions, :session_data, :json, default: {}, null: false unless column_exists?(:action_mcp_sessions, :session_data)
|
|
26
|
+
|
|
27
|
+
create_table :action_mcp_session_messages, if_not_exists: true do |t|
|
|
28
|
+
t.references :session, null: false,
|
|
29
|
+
foreign_key: { to_table: :action_mcp_sessions,
|
|
30
|
+
on_delete: :cascade,
|
|
31
|
+
on_update: :cascade,
|
|
32
|
+
name: "fk_action_mcp_session_messages_session_id" },
|
|
33
|
+
type: :string
|
|
34
|
+
t.string :direction, null: false, comment: "The message recipient", default: "client"
|
|
35
|
+
t.string :message_type, null: false, comment: "The type of the message"
|
|
36
|
+
t.string :jsonrpc_id
|
|
37
|
+
t.json :message_json
|
|
38
|
+
t.boolean :is_ping, default: false, null: false, comment: "Whether the message is a ping"
|
|
39
|
+
t.boolean :request_acknowledged, default: false, null: false
|
|
40
|
+
t.boolean :request_cancelled, null: false, default: false
|
|
41
|
+
t.timestamps
|
|
86
42
|
end
|
|
87
43
|
|
|
88
|
-
unless column_exists?(:action_mcp_session_messages, :
|
|
89
|
-
|
|
44
|
+
add_column :action_mcp_session_messages, :is_ping, :boolean, default: false, null: false unless column_exists?(:action_mcp_session_messages, :is_ping)
|
|
45
|
+
add_column :action_mcp_session_messages, :request_acknowledged, :boolean, default: false, null: false unless column_exists?(:action_mcp_session_messages, :request_acknowledged)
|
|
46
|
+
add_column :action_mcp_session_messages, :request_cancelled, :boolean, default: false, null: false unless column_exists?(:action_mcp_session_messages, :request_cancelled)
|
|
47
|
+
|
|
48
|
+
create_table :action_mcp_session_subscriptions, if_not_exists: true do |t|
|
|
49
|
+
t.references :session, null: false,
|
|
50
|
+
foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
|
|
51
|
+
type: :string
|
|
52
|
+
t.string :uri, null: false
|
|
53
|
+
t.datetime :last_notification_at
|
|
54
|
+
t.timestamps
|
|
90
55
|
end
|
|
91
56
|
|
|
92
|
-
|
|
93
|
-
|
|
57
|
+
create_table :action_mcp_session_tasks, id: :string, if_not_exists: true do |t|
|
|
58
|
+
t.references :session, null: false,
|
|
59
|
+
foreign_key: { to_table: :action_mcp_sessions,
|
|
60
|
+
on_delete: :cascade,
|
|
61
|
+
on_update: :cascade,
|
|
62
|
+
name: "fk_action_mcp_session_tasks_session_id" },
|
|
63
|
+
type: :string
|
|
64
|
+
t.string :status, null: false, default: "working"
|
|
65
|
+
t.string :status_message
|
|
66
|
+
t.string :request_method, comment: "e.g., tools/call, prompts/get"
|
|
67
|
+
t.string :request_name, comment: "e.g., tool name, prompt name"
|
|
68
|
+
t.json :request_params, comment: "Original request params"
|
|
69
|
+
t.json :result_payload, comment: "Final result data"
|
|
70
|
+
t.integer :ttl, comment: "Time to live in milliseconds"
|
|
71
|
+
t.integer :poll_interval, comment: "Suggested polling interval in milliseconds"
|
|
72
|
+
t.datetime :last_updated_at, null: false
|
|
73
|
+
t.json :continuation_state, default: {}
|
|
74
|
+
t.integer :progress_percent, comment: "Task progress as percentage 0-100"
|
|
75
|
+
t.string :progress_message, comment: "Human-readable progress message"
|
|
76
|
+
t.datetime :last_step_at
|
|
77
|
+
t.timestamps
|
|
78
|
+
|
|
79
|
+
t.index :status
|
|
80
|
+
t.index %i[session_id status]
|
|
81
|
+
t.index :created_at
|
|
94
82
|
end
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
add_column :action_mcp_session_tasks, :continuation_state, :json, default: {} unless column_exists?(:action_mcp_session_tasks, :continuation_state)
|
|
85
|
+
add_column :action_mcp_session_tasks, :progress_percent, :integer unless column_exists?(:action_mcp_session_tasks, :progress_percent)
|
|
86
|
+
add_column :action_mcp_session_tasks, :progress_message, :string unless column_exists?(:action_mcp_session_tasks, :progress_message)
|
|
87
|
+
add_column :action_mcp_session_tasks, :last_step_at, :datetime unless column_exists?(:action_mcp_session_tasks, :last_step_at)
|
|
88
|
+
|
|
89
|
+
# Remove deprecated tables if they exist
|
|
90
|
+
drop_table :action_mcp_oauth_clients, if_exists: true
|
|
91
|
+
drop_table :action_mcp_oauth_tokens, if_exists: true
|
|
92
|
+
drop_table :action_mcp_sse_events, if_exists: true
|
|
93
|
+
drop_table :action_mcp_session_resources, if_exists: true
|
|
94
|
+
|
|
95
|
+
# Remove deprecated columns if they exist
|
|
96
|
+
remove_column :action_mcp_sessions, :oauth_access_token if column_exists?(:action_mcp_sessions, :oauth_access_token)
|
|
97
|
+
remove_column :action_mcp_sessions, :oauth_refresh_token if column_exists?(:action_mcp_sessions, :oauth_refresh_token)
|
|
98
|
+
remove_column :action_mcp_sessions, :oauth_token_expires_at if column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
|
|
99
|
+
remove_column :action_mcp_sessions, :oauth_user_context if column_exists?(:action_mcp_sessions, :oauth_user_context)
|
|
100
|
+
remove_column :action_mcp_sessions, :sse_event_counter if column_exists?(:action_mcp_sessions, :sse_event_counter)
|
|
112
101
|
end
|
|
113
102
|
end
|
|
@@ -25,7 +25,6 @@ module ActionMCP
|
|
|
25
25
|
:logging_level,
|
|
26
26
|
:active_profile,
|
|
27
27
|
:profiles,
|
|
28
|
-
:elicitation_enabled,
|
|
29
28
|
:verbose_logging,
|
|
30
29
|
# --- Authentication Options ---
|
|
31
30
|
:authentication_methods,
|
|
@@ -58,7 +57,6 @@ module ActionMCP
|
|
|
58
57
|
@list_changed = true
|
|
59
58
|
@logging_level = :warning
|
|
60
59
|
@resources_subscribe = false
|
|
61
|
-
@elicitation_enabled = false
|
|
62
60
|
@verbose_logging = false
|
|
63
61
|
@active_profile = :primary
|
|
64
62
|
@profiles = default_profiles
|
|
@@ -73,6 +71,10 @@ module ActionMCP
|
|
|
73
71
|
@tasks_list_enabled = true
|
|
74
72
|
@tasks_cancel_enabled = true
|
|
75
73
|
|
|
74
|
+
# Pagination - nil means off. Set a number to enable with that page size.
|
|
75
|
+
# Most MCP clients (including Claude Code) don't follow nextCursor yet.
|
|
76
|
+
@pagination_page_size = nil
|
|
77
|
+
|
|
76
78
|
# Schema validation - disabled by default for backward compatibility
|
|
77
79
|
@validate_structured_content = false
|
|
78
80
|
|
|
@@ -132,6 +134,19 @@ module ActionMCP
|
|
|
132
134
|
@allowed_identity_keys = Array(value).map(&:to_s).freeze
|
|
133
135
|
end
|
|
134
136
|
|
|
137
|
+
# Pagination page size. nil = pagination disabled, positive integer = enabled.
|
|
138
|
+
attr_reader :pagination_page_size
|
|
139
|
+
|
|
140
|
+
def pagination_page_size=(value)
|
|
141
|
+
if value.nil?
|
|
142
|
+
@pagination_page_size = nil
|
|
143
|
+
else
|
|
144
|
+
size = value.to_i
|
|
145
|
+
raise ArgumentError, "pagination_page_size must be a positive integer, got: #{value.inspect}" unless size > 0
|
|
146
|
+
@pagination_page_size = size
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
135
150
|
def gateway_class
|
|
136
151
|
# Resolve gateway class lazily to account for Zeitwerk autoloading
|
|
137
152
|
# This allows ApplicationGateway to be loaded from app/mcp even if the
|
|
@@ -263,7 +278,6 @@ module ActionMCP
|
|
|
263
278
|
capabilities[:resources] = { subscribe: @resources_subscribe, listChanged: @list_changed }
|
|
264
279
|
end
|
|
265
280
|
|
|
266
|
-
capabilities[:elicitation] = {} if @elicitation_enabled
|
|
267
281
|
|
|
268
282
|
# Tasks capability (MCP 2025-11-25)
|
|
269
283
|
if @tasks_enabled
|
|
@@ -383,6 +397,11 @@ module ActionMCP
|
|
|
383
397
|
if config["server_instructions"]
|
|
384
398
|
@server_instructions = parse_instructions(config["server_instructions"])
|
|
385
399
|
end
|
|
400
|
+
|
|
401
|
+
# Extract pagination page size (nil = off, positive integer = on)
|
|
402
|
+
if config.key?("pagination_page_size")
|
|
403
|
+
self.pagination_page_size = config["pagination_page_size"]
|
|
404
|
+
end
|
|
386
405
|
end
|
|
387
406
|
|
|
388
407
|
def should_include_all?(type)
|
|
@@ -9,7 +9,8 @@ module ActionMCP
|
|
|
9
9
|
# @return [String] The MIME type of the resource.
|
|
10
10
|
# @return [String, nil] The text content of the resource (optional).
|
|
11
11
|
# @return [String, nil] The base64-encoded blob of the resource (optional).
|
|
12
|
-
|
|
12
|
+
# @return [Hash, nil] Optional extension metadata (serialized on the wire as `_meta`).
|
|
13
|
+
attr_reader :uri, :mime_type, :text, :blob, :annotations, :meta
|
|
13
14
|
|
|
14
15
|
# Initializes a new Resource content.
|
|
15
16
|
#
|
|
@@ -18,17 +19,30 @@ module ActionMCP
|
|
|
18
19
|
# @param text [String, nil] The text content of the resource (optional).
|
|
19
20
|
# @param blob [String, nil] The base64-encoded blob of the resource (optional).
|
|
20
21
|
# @param annotations [Hash, nil] Optional annotations for the resource.
|
|
21
|
-
|
|
22
|
+
# @param meta [Hash, #to_hash, #to_h, nil] Optional extension metadata. Emitted on the wire as `_meta`.
|
|
23
|
+
def initialize(uri, mime_type = "text/plain", text: nil, blob: nil, annotations: nil, meta: nil)
|
|
22
24
|
super("resource", annotations: annotations)
|
|
23
25
|
@uri = uri
|
|
24
26
|
@mime_type = mime_type
|
|
25
27
|
@text = text
|
|
26
28
|
@blob = blob
|
|
27
29
|
@annotations = annotations
|
|
30
|
+
@meta =
|
|
31
|
+
if meta.nil?
|
|
32
|
+
nil
|
|
33
|
+
elsif meta.respond_to?(:to_hash)
|
|
34
|
+
meta.to_hash
|
|
35
|
+
elsif meta.respond_to?(:to_h)
|
|
36
|
+
meta.to_h
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, "meta must respond to :to_hash or :to_h, got: #{meta.class}"
|
|
39
|
+
end
|
|
28
40
|
end
|
|
29
41
|
|
|
30
42
|
# Returns a hash representation of the resource content.
|
|
31
43
|
# Per MCP spec, embedded resources have type "resource" with a nested resource object.
|
|
44
|
+
# `meta` is emitted as `_meta` on the inner resource hash (TextResourceContents /
|
|
45
|
+
# BlobResourceContents), not on the outer content envelope.
|
|
32
46
|
#
|
|
33
47
|
# @return [Hash] The hash representation of the resource content.
|
|
34
48
|
def to_h
|
|
@@ -36,6 +50,7 @@ module ActionMCP
|
|
|
36
50
|
inner[:text] = @text if @text
|
|
37
51
|
inner[:blob] = @blob if @blob
|
|
38
52
|
inner[:annotations] = @annotations if @annotations
|
|
53
|
+
inner[:_meta] = @meta if @meta && !@meta.empty?
|
|
39
54
|
|
|
40
55
|
{ type: @type, resource: inner }
|
|
41
56
|
end
|