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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be05e5d56452910d6c725b15041b68b41b12830f38bd5661ba5c363403ef8df7
4
- data.tar.gz: 4ead1672cb76e8c1ff4057062f6396cc68d290abf0ab7c0e0562f2d438ddd8a7
3
+ metadata.gz: 917d5120acc0a5cb82cb4a9516a65e59370f922dfe2828670e1117a5d1d26934
4
+ data.tar.gz: a77bd78234661ee4ef52cafff59f5d6c856e94728bca98832c926928635e8e94
5
5
  SHA512:
6
- metadata.gz: f0d3ed2611e1347f24a1b6422c9d1628d85cbce23e9b9db13996aca9d9133d03e50bf6b45b26a0f4afa5795314b5285e70e545ecfd2e40ef71294bfc13241b03
7
- data.tar.gz: fbdf20f97d3324d4adc6d9905ff45dc662ea9a039e6682e884a614fb02a14726b32c17c84785856188d9dea2babbe7aefd9d8d2d0c1c4171facaf69d1dcd02ce
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
- assert_tool_findable("calculate_sum")
990
- result = execute_tool("calculate_sum", a: 5, b: 10)
991
- assert_tool_output(result, "15.0")
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
- assert_prompt_findable("analyze_code")
996
- result = execute_prompt("analyze_code", language: "Ruby", code: "def hello; puts 'Hello, world!'; end")
997
- assert_equal "Analyzing Ruby code: def hello; puts 'Hello, world!'; end", assert_prompt_output(result)
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
- - `assert_tool_findable(name)` - Verifies a tool exists and is registered
1004
- - `assert_prompt_findable(name)` - Verifies a prompt exists and is registered
1005
- - `execute_tool(name, **args)` - Executes a tool with arguments
1006
- - `execute_prompt(name, **args)` - Executes a prompt with arguments
1007
- - `assert_tool_output(result, expected)` - Asserts tool output matches expected text
1008
- - `assert_prompt_output(result)` - Extracts and returns prompt output for assertions
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.0]
4
- def change
5
- # Only create tables if they don't exist to avoid deleting existing data
6
-
7
- # Create sessions table
8
- unless table_exists?(:action_mcp_sessions)
9
- create_table :action_mcp_sessions, id: :string do |t|
10
- t.string :role, null: false, default: 'server', comment: 'The role of the session'
11
- t.string :status, null: false, default: 'pre_initialize'
12
- t.datetime :ended_at, comment: 'The time the session ended'
13
- t.string :protocol_version
14
- t.json :server_capabilities, comment: 'The capabilities of the server'
15
- t.json :client_capabilities, comment: 'The capabilities of the client'
16
- t.json :server_info, comment: 'The information about the server'
17
- t.json :client_info, comment: 'The information about the client'
18
- t.boolean :initialized, null: false, default: false
19
- t.integer :messages_count, null: false, default: 0
20
- t.json :tool_registry, default: []
21
- t.json :prompt_registry, default: []
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
- # For action_mcp_session_messages
77
- return unless table_exists?(:action_mcp_session_messages)
78
-
79
- unless column_exists?(:action_mcp_session_messages, :is_ping)
80
- add_column :action_mcp_session_messages, :is_ping, :boolean, default: false, null: false,
81
- comment: 'Whether the message is a ping'
82
- end
83
-
84
- unless column_exists?(:action_mcp_session_messages, :request_acknowledged)
85
- add_column :action_mcp_session_messages, :request_acknowledged, :boolean, default: false, null: false
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, :request_cancelled)
89
- add_column :action_mcp_session_messages, :request_cancelled, :boolean, null: false, default: false
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
- if column_exists?(:action_mcp_session_messages, :message_text)
93
- remove_column :action_mcp_session_messages, :message_text
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
- return unless column_exists?(:action_mcp_session_messages, :direction)
97
-
98
- # SQLite3 doesn't support changing column comments
99
- return unless connection.adapter_name.downcase != 'sqlite'
100
-
101
- change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
102
- end
103
-
104
- private
105
-
106
- def table_exists?(table_name)
107
- ActionMCP::ApplicationRecord.connection.table_exists?(table_name)
108
- end
109
-
110
- def column_exists?(table_name, column_name)
111
- ActionMCP::ApplicationRecord.connection.column_exists?(table_name, column_name)
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"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.107.1"
5
+ VERSION = "0.108.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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.107.1
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.3
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,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddConsentsToActionMCPSess < ActiveRecord::Migration[8.0]
4
- def change
5
- return if column_exists?(:action_mcp_sessions, :consents)
6
-
7
- add_column :action_mcp_sessions, :consents, :json, default: {}, null: false
8
- end
9
- end
@@ -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