actionmcp 0.107.1 → 0.108.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 +58 -14
- data/app/controllers/action_mcp/application_controller.rb +5 -0
- data/app/models/action_mcp/session.rb +35 -3
- data/db/migrate/20250512154359_consolidated_migration.rb +90 -101
- data/lib/action_mcp/test_helper.rb +35 -0
- data/lib/action_mcp/version.rb +1 -1
- metadata +2 -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: 917d5120acc0a5cb82cb4a9516a65e59370f922dfe2828670e1117a5d1d26934
|
|
4
|
+
data.tar.gz: a77bd78234661ee4ef52cafff59f5d6c856e94728bca98832c926928635e8e94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6110ec397f08c3e8cbc2fe54a20f22bae3f600ad005e82f254bf6eeea7ba07c3bee1fc28a337a6263725fff213fdcbf092b08000285bdff0958b16fa5a958ccf
|
|
7
|
+
data.tar.gz: f57f192f165bf2a5d44d18189e6107eb0ab951eeaf4f88ddc92fa2e3208bcc4bfe7a7d6aed9cd0e01b2951d07e6e8b2826f75e6033054130b304707b0bec03b1
|
data/README.md
CHANGED
|
@@ -712,10 +712,12 @@ This will create:
|
|
|
712
712
|
|
|
713
713
|
## Authentication with Gateway
|
|
714
714
|
|
|
715
|
-
ActionMCP provides a Gateway system for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
|
|
715
|
+
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
716
|
|
|
717
717
|
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
718
|
|
|
719
|
+
> **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.
|
|
720
|
+
|
|
719
721
|
### Creating an ApplicationGateway
|
|
720
722
|
|
|
721
723
|
When you run the install generator, it creates an `ApplicationGateway` class:
|
|
@@ -986,26 +988,68 @@ class ToolTest < ActiveSupport::TestCase
|
|
|
986
988
|
include ActionMCP::TestHelper
|
|
987
989
|
|
|
988
990
|
test "CalculateSumTool returns the correct sum" do
|
|
989
|
-
|
|
990
|
-
result =
|
|
991
|
-
|
|
991
|
+
assert_mcp_tool_findable("calculate_sum")
|
|
992
|
+
result = execute_mcp_tool("calculate_sum", a: 5, b: 10)
|
|
993
|
+
assert_mcp_tool_output("15.0", result)
|
|
992
994
|
end
|
|
993
995
|
|
|
994
996
|
test "AnalyzeCodePrompt returns the correct analysis" do
|
|
995
|
-
|
|
996
|
-
result =
|
|
997
|
-
|
|
997
|
+
assert_mcp_prompt_findable("analyze_code")
|
|
998
|
+
result = execute_mcp_prompt("analyze_code", language: "Ruby", code: "def hello; puts 'Hello, world!'; end")
|
|
999
|
+
assert_mcp_prompt_output("Analyzing Ruby code: def hello; puts 'Hello, world!'; end", result)
|
|
998
1000
|
end
|
|
999
1001
|
end
|
|
1000
1002
|
```
|
|
1001
1003
|
|
|
1002
|
-
The TestHelper provides several assertion methods:
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
- `
|
|
1006
|
-
- `
|
|
1007
|
-
- `
|
|
1008
|
-
- `
|
|
1004
|
+
The TestHelper provides several assertion and execution methods:
|
|
1005
|
+
|
|
1006
|
+
**Tools:**
|
|
1007
|
+
- `assert_mcp_tool_findable(name)` - Verifies a tool exists and is registered
|
|
1008
|
+
- `execute_mcp_tool(name, **args)` - Executes a tool with arguments and asserts success
|
|
1009
|
+
- `execute_mcp_tool_with_error(name, **args)` - Executes a tool without asserting success (for testing error cases)
|
|
1010
|
+
- `assert_mcp_tool_output(expected, response)` - Asserts tool output matches expected content
|
|
1011
|
+
|
|
1012
|
+
**Prompts:**
|
|
1013
|
+
- `assert_mcp_prompt_findable(name)` - Verifies a prompt exists and is registered
|
|
1014
|
+
- `execute_mcp_prompt(name, **args)` - Executes a prompt with arguments
|
|
1015
|
+
- `assert_mcp_prompt_output(expected, response)` - Asserts prompt output matches expected content
|
|
1016
|
+
|
|
1017
|
+
**Resource Templates:**
|
|
1018
|
+
- `assert_mcp_resource_template_findable(name)` - Verifies a resource template exists and is registered
|
|
1019
|
+
- `resolve_mcp_resource(uri)` - Resolves a resource URI and asserts success
|
|
1020
|
+
- `resolve_mcp_resource_with_error(uri)` - Resolves a resource URI without asserting success (for testing error cases)
|
|
1021
|
+
|
|
1022
|
+
**General:**
|
|
1023
|
+
- `assert_mcp_error_code(code, response)` - Asserts a specific JSON-RPC error code
|
|
1024
|
+
|
|
1025
|
+
### Testing Resource Templates
|
|
1026
|
+
|
|
1027
|
+
```ruby
|
|
1028
|
+
require "test_helper"
|
|
1029
|
+
require "action_mcp/test_helper"
|
|
1030
|
+
|
|
1031
|
+
class ProductResourceTest < ActiveSupport::TestCase
|
|
1032
|
+
include ActionMCP::TestHelper
|
|
1033
|
+
|
|
1034
|
+
test "product template is registered" do
|
|
1035
|
+
assert_mcp_resource_template_findable("products")
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
test "resolves a product resource by URI" do
|
|
1039
|
+
resp = resolve_mcp_resource("ecommerce://products/1")
|
|
1040
|
+
|
|
1041
|
+
assert resp.success?
|
|
1042
|
+
assert_not_empty resp.contents
|
|
1043
|
+
assert_equal "application/json", resp.contents.first.mime_type
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
test "returns error for nonexistent product" do
|
|
1047
|
+
resp = resolve_mcp_resource_with_error("ecommerce://products/0")
|
|
1048
|
+
|
|
1049
|
+
assert resp.is_error
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
```
|
|
1009
1053
|
|
|
1010
1054
|
## Inspecting Your MCP Server
|
|
1011
1055
|
|
|
@@ -70,6 +70,11 @@ module ActionMCP
|
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
if session.nil?
|
|
74
|
+
id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
|
|
75
|
+
return render_not_found("Session not found.", id)
|
|
76
|
+
end
|
|
77
|
+
|
|
73
78
|
if session.new_record?
|
|
74
79
|
session.save!
|
|
75
80
|
response.headers[MCP_SESSION_ID_HEADER] = session.id
|
|
@@ -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
|
|
@@ -14,8 +14,12 @@ module ActionMCP
|
|
|
14
14
|
#
|
|
15
15
|
# and you get assert_mcp_tool_findable,
|
|
16
16
|
# assert_mcp_prompt_findable,
|
|
17
|
+
# assert_mcp_resource_template_findable,
|
|
17
18
|
# execute_mcp_tool,
|
|
19
|
+
# execute_mcp_tool_with_error,
|
|
18
20
|
# execute_mcp_prompt,
|
|
21
|
+
# resolve_mcp_resource,
|
|
22
|
+
# resolve_mcp_resource_with_error,
|
|
19
23
|
# assert_mcp_error_code,
|
|
20
24
|
# assert_mcp_tool_output,
|
|
21
25
|
# assert_mcp_prompt_output.
|
|
@@ -29,6 +33,12 @@ module ActionMCP
|
|
|
29
33
|
include ProgressNotificationAssertions
|
|
30
34
|
|
|
31
35
|
# ──── Registry assertions ────────────────────────────────────────────────
|
|
36
|
+
def assert_mcp_resource_template_findable(name, msg = nil)
|
|
37
|
+
assert ActionMCP::ResourceTemplatesRegistry.resource_templates.key?(name),
|
|
38
|
+
msg || "Resource template #{name.inspect} not found in ResourceTemplatesRegistry"
|
|
39
|
+
end
|
|
40
|
+
alias assert_resource_template_findable assert_mcp_resource_template_findable
|
|
41
|
+
|
|
32
42
|
def assert_mcp_tool_findable(name, msg = nil)
|
|
33
43
|
assert ActionMCP::ToolsRegistry.tools.key?(name),
|
|
34
44
|
msg || "Tool #{name.inspect} not found in ToolsRegistry"
|
|
@@ -61,6 +71,31 @@ module ActionMCP
|
|
|
61
71
|
end
|
|
62
72
|
alias execute_prompt execute_mcp_prompt
|
|
63
73
|
|
|
74
|
+
def resolve_mcp_resource(uri)
|
|
75
|
+
template_class = ActionMCP::ResourceTemplatesRegistry.find_template_for_uri(uri)
|
|
76
|
+
assert template_class, "No resource template found matching URI #{uri.inspect}"
|
|
77
|
+
template = template_class.process(uri)
|
|
78
|
+
assert template, "Failed to process URI #{uri.inspect} with template #{template_class.name}"
|
|
79
|
+
resp = template.call
|
|
80
|
+
assert !resp.is_error, "Resource #{uri.inspect} returned error: #{resp.to_h[:message]}"
|
|
81
|
+
resp
|
|
82
|
+
end
|
|
83
|
+
alias resolve_resource resolve_mcp_resource
|
|
84
|
+
|
|
85
|
+
def resolve_mcp_resource_with_error(uri)
|
|
86
|
+
template_class = ActionMCP::ResourceTemplatesRegistry.find_template_for_uri(uri)
|
|
87
|
+
unless template_class
|
|
88
|
+
return ActionMCP::ResourceResponse.new.tap { |r| r.mark_as_not_found!(uri) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless template_class.respond_to?(:readable_uri?) && template_class.readable_uri?(uri)
|
|
92
|
+
return ActionMCP::ResourceResponse.new.tap { |r| r.mark_as_not_found!(uri) }
|
|
93
|
+
end
|
|
94
|
+
template = template_class.process(uri)
|
|
95
|
+
template.call
|
|
96
|
+
end
|
|
97
|
+
alias resolve_resource_with_error resolve_mcp_resource_with_error
|
|
98
|
+
|
|
64
99
|
# ──── Negative‑path helper ───────────────────────────────────────────────
|
|
65
100
|
def assert_mcp_error_code(code, response, msg = nil)
|
|
66
101
|
assert response.error?, msg || "Expected response to be an error"
|
data/lib/action_mcp/version.rb
CHANGED
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.108.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -160,14 +160,6 @@ files:
|
|
|
160
160
|
- app/models/concerns/action_mcp/mcp_message_inspect.rb
|
|
161
161
|
- config/routes.rb
|
|
162
162
|
- db/migrate/20250512154359_consolidated_migration.rb
|
|
163
|
-
- db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb
|
|
164
|
-
- db/migrate/20250727000001_remove_oauth_support.rb
|
|
165
|
-
- db/migrate/20251125000001_create_action_mcp_session_tasks.rb
|
|
166
|
-
- db/migrate/20251126000001_add_continuation_state_to_action_mcp_session_tasks.rb
|
|
167
|
-
- db/migrate/20251203000001_remove_sse_support.rb
|
|
168
|
-
- db/migrate/20251204000001_add_progress_to_session_tasks.rb
|
|
169
|
-
- db/migrate/20260303000001_add_session_data_to_action_mcp_sessions.rb
|
|
170
|
-
- db/migrate/20260304000001_remove_session_resources.rb
|
|
171
163
|
- db/test.sqlite3
|
|
172
164
|
- exe/actionmcp_cli
|
|
173
165
|
- lib/action_mcp.rb
|
|
@@ -303,7 +295,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
303
295
|
- !ruby/object:Gem::Version
|
|
304
296
|
version: '0'
|
|
305
297
|
requirements: []
|
|
306
|
-
rubygems_version: 4.0.
|
|
298
|
+
rubygems_version: 4.0.6
|
|
307
299
|
specification_version: 4
|
|
308
300
|
summary: Lightweight Model Context Protocol (MCP) server toolkit for Ruby/Rails
|
|
309
301
|
test_files: []
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class RemoveOauthSupport < ActiveRecord::Migration[8.0]
|
|
4
|
-
def change
|
|
5
|
-
# Remove OAuth tables
|
|
6
|
-
drop_table :action_mcp_oauth_clients, if_exists: true do |t|
|
|
7
|
-
t.string :client_id, null: false
|
|
8
|
-
t.string :client_secret
|
|
9
|
-
t.string :client_name
|
|
10
|
-
t.json :redirect_uris, default: []
|
|
11
|
-
t.json :grant_types, default: [ "authorization_code" ]
|
|
12
|
-
t.json :response_types, default: [ "code" ]
|
|
13
|
-
t.string :token_endpoint_auth_method, default: "client_secret_basic"
|
|
14
|
-
t.text :scope
|
|
15
|
-
t.boolean :active, default: true
|
|
16
|
-
t.integer :client_id_issued_at
|
|
17
|
-
t.integer :client_secret_expires_at
|
|
18
|
-
t.string :registration_access_token
|
|
19
|
-
t.json :metadata, default: {}
|
|
20
|
-
t.datetime :created_at, null: false
|
|
21
|
-
t.datetime :updated_at, null: false
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
drop_table :action_mcp_oauth_tokens, if_exists: true do |t|
|
|
25
|
-
t.string :token, null: false
|
|
26
|
-
t.string :token_type, null: false
|
|
27
|
-
t.string :client_id, null: false
|
|
28
|
-
t.string :user_id
|
|
29
|
-
t.text :scope
|
|
30
|
-
t.datetime :expires_at
|
|
31
|
-
t.boolean :revoked, default: false
|
|
32
|
-
t.string :redirect_uri
|
|
33
|
-
t.string :code_challenge
|
|
34
|
-
t.string :code_challenge_method
|
|
35
|
-
t.string :access_token
|
|
36
|
-
t.json :metadata, default: {}
|
|
37
|
-
t.datetime :created_at, null: false
|
|
38
|
-
t.datetime :updated_at, null: false
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Remove OAuth columns from sessions table
|
|
42
|
-
if table_exists?(:action_mcp_sessions)
|
|
43
|
-
remove_column :action_mcp_sessions, :oauth_access_token, :string if column_exists?(:action_mcp_sessions, :oauth_access_token)
|
|
44
|
-
remove_column :action_mcp_sessions, :oauth_refresh_token, :string if column_exists?(:action_mcp_sessions, :oauth_refresh_token)
|
|
45
|
-
remove_column :action_mcp_sessions, :oauth_token_expires_at, :datetime if column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
|
|
46
|
-
remove_column :action_mcp_sessions, :oauth_user_context, :json if column_exists?(:action_mcp_sessions, :oauth_user_context)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def table_exists?(table_name)
|
|
53
|
-
ActiveRecord::Base.connection.table_exists?(table_name)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def column_exists?(table_name, column_name)
|
|
57
|
-
ActiveRecord::Base.connection.column_exists?(table_name, column_name)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateActionMCPSessionTasks < ActiveRecord::Migration[8.0]
|
|
4
|
-
def change
|
|
5
|
-
create_table :action_mcp_session_tasks, id: :string do |t|
|
|
6
|
-
t.references :session, null: false,
|
|
7
|
-
foreign_key: { to_table: :action_mcp_sessions,
|
|
8
|
-
on_delete: :cascade,
|
|
9
|
-
on_update: :cascade,
|
|
10
|
-
name: 'fk_action_mcp_session_tasks_session_id' },
|
|
11
|
-
type: :string
|
|
12
|
-
t.string :status, null: false, default: 'working'
|
|
13
|
-
t.string :status_message
|
|
14
|
-
t.string :request_method, comment: 'e.g., tools/call, prompts/get'
|
|
15
|
-
t.string :request_name, comment: 'e.g., tool name, prompt name'
|
|
16
|
-
t.json :request_params, comment: 'Original request params'
|
|
17
|
-
t.json :result_payload, comment: 'Final result data'
|
|
18
|
-
t.integer :ttl, comment: 'Time to live in milliseconds'
|
|
19
|
-
t.integer :poll_interval, comment: 'Suggested polling interval in milliseconds'
|
|
20
|
-
t.datetime :last_updated_at, null: false
|
|
21
|
-
|
|
22
|
-
t.timestamps
|
|
23
|
-
|
|
24
|
-
t.index :status
|
|
25
|
-
t.index %i[session_id status]
|
|
26
|
-
t.index :created_at
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class AddContinuationStateToActionMCPSessionTasks < ActiveRecord::Migration[8.1]
|
|
4
|
-
def change
|
|
5
|
-
add_column :action_mcp_session_tasks, :continuation_state, :json, default: {}
|
|
6
|
-
add_column :action_mcp_session_tasks, :progress_percent, :integer
|
|
7
|
-
add_column :action_mcp_session_tasks, :progress_message, :string
|
|
8
|
-
add_column :action_mcp_session_tasks, :last_step_at, :datetime
|
|
9
|
-
end
|
|
10
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class RemoveSseSupport < ActiveRecord::Migration[8.1]
|
|
4
|
-
def up
|
|
5
|
-
# Drop SSE events table
|
|
6
|
-
drop_table :action_mcp_sse_events, if_exists: true
|
|
7
|
-
|
|
8
|
-
# Remove SSE counter from sessions
|
|
9
|
-
remove_column :action_mcp_sessions, :sse_event_counter, if_exists: true
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def down
|
|
13
|
-
# Re-add sse_event_counter to sessions
|
|
14
|
-
unless column_exists?(:action_mcp_sessions, :sse_event_counter)
|
|
15
|
-
add_column :action_mcp_sessions, :sse_event_counter, :integer, default: 0, null: false
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Re-create SSE events table
|
|
19
|
-
unless table_exists?(:action_mcp_sse_events)
|
|
20
|
-
create_table :action_mcp_sse_events do |t|
|
|
21
|
-
t.references :session, null: false, type: :string, foreign_key: { to_table: :action_mcp_sessions }
|
|
22
|
-
t.integer :event_id, null: false
|
|
23
|
-
t.text :data, null: false
|
|
24
|
-
t.timestamps
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
add_index :action_mcp_sse_events, :created_at
|
|
28
|
-
add_index :action_mcp_sse_events, [ :session_id, :event_id ], unique: true
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class AddProgressToSessionTasks < ActiveRecord::Migration[8.1]
|
|
4
|
-
def change
|
|
5
|
-
unless column_exists?(:action_mcp_session_tasks, :progress_percent)
|
|
6
|
-
add_column :action_mcp_session_tasks, :progress_percent, :integer, comment: "Task progress as percentage 0-100"
|
|
7
|
-
end
|
|
8
|
-
unless column_exists?(:action_mcp_session_tasks, :progress_message)
|
|
9
|
-
add_column :action_mcp_session_tasks, :progress_message, :string, comment: "Human-readable progress message"
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class AddSessionDataToActionMCPSessions < ActiveRecord::Migration[8.1]
|
|
4
|
-
def change
|
|
5
|
-
return if column_exists?(:action_mcp_sessions, :session_data)
|
|
6
|
-
|
|
7
|
-
add_column :action_mcp_sessions, :session_data, :json, default: {}, null: false
|
|
8
|
-
end
|
|
9
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class RemoveSessionResources < ActiveRecord::Migration[8.1]
|
|
4
|
-
def up
|
|
5
|
-
drop_table :action_mcp_session_resources, if_exists: true
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def down
|
|
9
|
-
return if table_exists?(:action_mcp_session_resources)
|
|
10
|
-
|
|
11
|
-
create_table :action_mcp_session_resources do |t|
|
|
12
|
-
t.references :session,
|
|
13
|
-
null: false,
|
|
14
|
-
foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
|
|
15
|
-
type: :string
|
|
16
|
-
t.string :uri, null: false
|
|
17
|
-
t.string :name
|
|
18
|
-
t.text :description
|
|
19
|
-
t.string :mime_type, null: false
|
|
20
|
-
t.boolean :created_by_tool, default: false
|
|
21
|
-
t.datetime :last_accessed_at
|
|
22
|
-
t.json :metadata
|
|
23
|
-
t.timestamps
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|