actionmcp 0.83.4 → 0.100.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/action_mcp/application_controller.rb +12 -222
  3. data/app/jobs/action_mcp/tool_execution_job.rb +133 -0
  4. data/app/models/action_mcp/session/task.rb +204 -0
  5. data/app/models/action_mcp/session.rb +2 -65
  6. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +2 -0
  7. data/db/migrate/20251125000001_create_action_mcp_session_tasks.rb +29 -0
  8. data/db/migrate/20251126000001_add_continuation_state_to_action_mcp_session_tasks.rb +10 -0
  9. data/db/migrate/20251203000001_remove_sse_support.rb +31 -0
  10. data/db/migrate/20251204000001_add_progress_to_session_tasks.rb +12 -0
  11. data/db/test.sqlite3 +0 -0
  12. data/exe/actionmcp_cli +1 -1
  13. data/lib/action_mcp/capability.rb +1 -0
  14. data/lib/action_mcp/client/base.rb +1 -1
  15. data/lib/action_mcp/configuration.rb +22 -15
  16. data/lib/action_mcp/engine.rb +8 -1
  17. data/lib/action_mcp/filtered_logger.rb +0 -3
  18. data/lib/action_mcp/json_rpc_handler_base.rb +10 -0
  19. data/lib/action_mcp/prompt.rb +16 -0
  20. data/lib/action_mcp/registry_base.rb +23 -1
  21. data/lib/action_mcp/server/base_session.rb +1 -71
  22. data/lib/action_mcp/server/handlers/router.rb +2 -0
  23. data/lib/action_mcp/server/handlers/task_handler.rb +86 -0
  24. data/lib/action_mcp/server/json_rpc_handler.rb +3 -0
  25. data/lib/action_mcp/server/simple_pub_sub.rb +1 -1
  26. data/lib/action_mcp/server/solid_mcp_adapter.rb +1 -1
  27. data/lib/action_mcp/server/tasks.rb +125 -0
  28. data/lib/action_mcp/server/tools.rb +47 -1
  29. data/lib/action_mcp/server/transport_handler.rb +1 -0
  30. data/lib/action_mcp/tool.rb +100 -0
  31. data/lib/action_mcp/version.rb +1 -1
  32. data/lib/action_mcp.rb +3 -4
  33. data/lib/tasks/action_mcp_tasks.rake +1 -35
  34. metadata +42 -7
  35. data/app/models/action_mcp/session/sse_event.rb +0 -62
  36. data/lib/action_mcp/sse_listener.rb +0 -81
@@ -73,8 +73,8 @@ module ActionMCP
73
73
  dependent: :delete_all,
74
74
  inverse_of: :session
75
75
 
76
- has_many :sse_events,
77
- class_name: "ActionMCP::Session::SSEEvent",
76
+ has_many :tasks,
77
+ class_name: "ActionMCP::Session::Task",
78
78
  foreign_key: "session_id",
79
79
  dependent: :delete_all,
80
80
  inverse_of: :session
@@ -178,57 +178,6 @@ module ActionMCP
178
178
  subscriptions.find_by(uri: uri)&.destroy
179
179
  end
180
180
 
181
- # Atomically increments the SSE event counter and returns the new value.
182
- # This ensures unique, sequential IDs for SSE events within the session.
183
- # @return [Integer] The new value of the counter.
184
- def increment_sse_counter!
185
- # Use update_counters for an atomic increment operation
186
- self.class.update_counters(id, sse_event_counter: 1)
187
- # Reload to get the updated value (update_counters doesn't update the instance)
188
- reload.sse_event_counter
189
- end
190
-
191
- # Stores an SSE event for potential resumption
192
- # @param event_id [Integer] The event ID
193
- # @param data [Hash, String] The event data
194
- # @param max_events [Integer] Maximum number of events to store (oldest events are removed when exceeded)
195
- # @return [ActionMCP::Session::SSEEvent] The created event
196
- def store_sse_event(event_id, data, max_events = 100)
197
- # Create the SSE event record
198
- event = sse_events.create!(
199
- event_id: event_id,
200
- data: data
201
- )
202
-
203
- # Maintain cache limit by removing oldest events if needed
204
- count = sse_events.count
205
- excess = count - max_events
206
- sse_events.order(event_id: :asc).limit(excess).delete_all if excess.positive?
207
-
208
- event
209
- end
210
-
211
- # Retrieves SSE events after a given ID
212
- # @param last_event_id [Integer] The ID to retrieve events after
213
- # @param limit [Integer] Maximum number of events to return
214
- # @return [Array<ActionMCP::Session::SSEEvent>] The events
215
- def get_sse_events_after(last_event_id, limit = 50)
216
- sse_events.where("event_id > ?", last_event_id)
217
- .order(event_id: :asc)
218
- .limit(limit)
219
- end
220
-
221
- # Cleans up old SSE events
222
- # @param max_age [ActiveSupport::Duration] Maximum age of events to keep
223
- # @return [Integer] Number of events removed
224
- def cleanup_old_sse_events(max_age = 15.minutes)
225
- cutoff_time = Time.current - max_age
226
- events_to_delete = sse_events.where("created_at < ?", cutoff_time)
227
- count = events_to_delete.count
228
- events_to_delete.destroy_all
229
- count
230
- end
231
-
232
181
  def send_progress_notification(progressToken:, progress:, total: nil, message: nil)
233
182
  # Create a transport handler to send the notification
234
183
  handler = ActionMCP::Server::TransportHandler.new(self)
@@ -240,18 +189,6 @@ module ActionMCP
240
189
  )
241
190
  end
242
191
 
243
- # Calculates the retention period for SSE events based on configuration
244
- # @return [ActiveSupport::Duration] The retention period
245
- def sse_event_retention_period
246
- ActionMCP.configuration.sse_event_retention_period || 15.minutes
247
- end
248
-
249
- # Calculates the maximum number of SSE events to store based on configuration
250
- # @return [Integer] The maximum number of events
251
- def max_stored_sse_events
252
- ActionMCP.configuration.max_stored_sse_events || 100
253
- end
254
-
255
192
  def send_progress_notification_legacy(token:, value:, message: nil)
256
193
  send_progress_notification(progressToken: token, progress: value, message: message)
257
194
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  class AddConsentsToActionMCPSess < ActiveRecord::Migration[8.0]
4
4
  def change
5
+ return if column_exists?(:action_mcp_sessions, :consents)
6
+
5
7
  add_column :action_mcp_sessions, :consents, :json, default: {}, null: false
6
8
  end
7
9
  end
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,12 @@
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
data/db/test.sqlite3 ADDED
File without changes
data/exe/actionmcp_cli CHANGED
@@ -62,7 +62,7 @@ end
62
62
 
63
63
  # Function to generate a unique request ID
64
64
  def generate_request_id
65
- SecureRandom.uuid
65
+ SecureRandom.uuid_v7
66
66
  end
67
67
 
68
68
  # Function to parse command shortcuts and return a Request object
@@ -11,6 +11,7 @@ module ActionMCP
11
11
  include Renderable
12
12
 
13
13
  class_attribute :_capability_name, instance_accessor: false
14
+ class_attribute :_registered_name, instance_accessor: false
14
15
  class_attribute :_description, instance_accessor: false, default: ""
15
16
 
16
17
  attr_reader :execution_context
@@ -187,7 +187,7 @@ module ActionMCP
187
187
  params[:sessionId] = @session_id if @session_id
188
188
 
189
189
  # Use a unique request ID (not session ID since we don't have one yet)
190
- request_id = SecureRandom.uuid
190
+ request_id = SecureRandom.uuid_v7
191
191
  send_jsonrpc_request("initialize", params: params, id: request_id)
192
192
  end
193
193
 
@@ -30,12 +30,7 @@ module ActionMCP
30
30
  # --- Authentication Options ---
31
31
  :authentication_methods,
32
32
  # --- Transport Options ---
33
- :sse_heartbeat_interval,
34
- :post_response_preference, # :json or :sse
35
33
  :protocol_version,
36
- # --- SSE Resumability Options ---
37
- :sse_event_retention_period,
38
- :max_stored_sse_events,
39
34
  # --- Gateway Options ---
40
35
  :gateway_class,
41
36
  # --- Session Store Options ---
@@ -48,7 +43,11 @@ module ActionMCP
48
43
  :max_threads,
49
44
  :max_queue,
50
45
  :polling_interval,
51
- :connects_to
46
+ :connects_to,
47
+ # --- Tasks Options (MCP 2025-11-25) ---
48
+ :tasks_enabled,
49
+ :tasks_list_enabled,
50
+ :tasks_cancel_enabled
52
51
 
53
52
  def initialize
54
53
  @logging_enabled = false
@@ -63,13 +62,12 @@ module ActionMCP
63
62
  # Authentication defaults - empty means all configured identifiers will be tried
64
63
  @authentication_methods = []
65
64
 
66
- @sse_heartbeat_interval = 30
67
- @post_response_preference = :json
68
- @protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
65
+ @protocol_version = "2025-06-18" # Default to stable version for backwards compatibility
69
66
 
70
- # Resumability defaults
71
- @sse_event_retention_period = 15.minutes
72
- @max_stored_sse_events = 100
67
+ # Tasks defaults (MCP 2025-11-25)
68
+ @tasks_enabled = false
69
+ @tasks_list_enabled = true
70
+ @tasks_cancel_enabled = true
73
71
 
74
72
  # Gateway - resolved lazily to account for Zeitwerk autoloading
75
73
  @gateway_class_name = nil
@@ -205,9 +203,6 @@ module ActionMCP
205
203
  capabilities = {}
206
204
  profile = @profiles[active_profile]
207
205
 
208
- Rails.logger.debug "[ActionMCP] Generating capabilities for profile: #{active_profile}"
209
- Rails.logger.debug "[ActionMCP] Profile config: #{profile.inspect}"
210
-
211
206
  # Check profile configuration instead of registry contents
212
207
  # If profile includes tools (either "all" or specific tools), advertise tools capability
213
208
  capabilities[:tools] = { listChanged: @list_changed } if profile && profile[:tools]&.any?
@@ -224,6 +219,18 @@ module ActionMCP
224
219
 
225
220
  capabilities[:elicitation] = {} if @elicitation_enabled
226
221
 
222
+ # Tasks capability (MCP 2025-11-25)
223
+ if @tasks_enabled
224
+ tasks_cap = {
225
+ requests: {
226
+ tools: { call: {} }
227
+ }
228
+ }
229
+ tasks_cap[:list] = {} if @tasks_list_enabled
230
+ tasks_cap[:cancel] = {} if @tasks_cancel_enabled
231
+ capabilities[:tasks] = tasks_cap
232
+ end
233
+
227
234
  capabilities
228
235
  end
229
236
 
@@ -10,7 +10,6 @@ module ActionMCP
10
10
  isolate_namespace ActionMCP
11
11
 
12
12
  ActiveSupport::Inflector.inflections(:en) do |inflect|
13
- inflect.acronym "SSE"
14
13
  inflect.acronym "MCP"
15
14
  end
16
15
 
@@ -61,6 +60,14 @@ module ActionMCP
61
60
 
62
61
  # Configure autoloading for the mcp/tools directory and identifiers
63
62
  initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
63
+ # Ensure ActionMCP base constants exist before Zeitwerk indexes app/mcp
64
+ # This prevents NameError when dependent gems have app/mcp
65
+ # directories with classes inheriting from ActionMCP::Tool, etc.
66
+ require "action_mcp/tool"
67
+ require "action_mcp/prompt"
68
+ require "action_mcp/resource_template"
69
+ require "action_mcp/gateway"
70
+
64
71
  mcp_path = app.root.join("app/mcp")
65
72
  identifiers_path = app.root.join("app/identifiers")
66
73
 
@@ -17,9 +17,6 @@ module ActionMCP
17
17
 
18
18
  # Filter out repetitive MCP notifications
19
19
  return if FILTERED_METHODS.any? { |method| message.include?(method) }
20
-
21
- # Filter out MCP protocol version debug messages
22
- return if message.include?("MCP-Protocol-Version header validation passed")
23
20
  end
24
21
 
25
22
  super
@@ -34,6 +34,16 @@ module ActionMCP
34
34
 
35
35
  # Elicitation methods
36
36
  ELICITATION_CREATE = "elicitation/create"
37
+
38
+ # Task methods (MCP 2025-11-25)
39
+ TASKS_GET = "tasks/get"
40
+ TASKS_RESULT = "tasks/result"
41
+ TASKS_LIST = "tasks/list"
42
+ TASKS_CANCEL = "tasks/cancel"
43
+ TASKS_RESUME = "tasks/resume"
44
+
45
+ # Task notifications
46
+ NOTIFICATIONS_TASKS_STATUS = "notifications/tasks/status"
37
47
  end
38
48
 
39
49
  delegate :initialize!, :initialized?, to: :transport
@@ -18,11 +18,27 @@ module ActionMCP
18
18
  def self.prompt_name(name = nil)
19
19
  if name
20
20
  self._capability_name = name
21
+ re_register_if_needed
22
+ name
21
23
  else
22
24
  _capability_name || default_prompt_name
23
25
  end
24
26
  end
25
27
 
28
+ # Re-registers the prompt if it was already registered under a different name
29
+ # @return [void]
30
+ def self.re_register_if_needed
31
+ return if abstract?
32
+ return unless _registered_name # Not yet registered
33
+
34
+ new_name = capability_name
35
+ return if _registered_name == new_name # No change
36
+
37
+ ActionMCP::PromptsRegistry.re_register(self, _registered_name)
38
+ end
39
+
40
+ private_class_method :re_register_if_needed
41
+
26
42
  # Returns the default prompt name based on the class name.
27
43
  #
28
44
  # @return [String] The default prompt name.
@@ -21,7 +21,29 @@ module ActionMCP
21
21
  def register(klass)
22
22
  return if klass.abstract?
23
23
 
24
- items[klass.capability_name] = klass
24
+ name = klass.capability_name
25
+ items[name] = klass
26
+ klass._registered_name = name if klass.respond_to?(:_registered_name=)
27
+ end
28
+
29
+ # Re-register an item under its current capability_name
30
+ # Removes old entry if name changed
31
+ #
32
+ # @param klass [Class] The class to re-register
33
+ # @param old_name [String] The previous registered name
34
+ # @return [void]
35
+ def re_register(klass, old_name)
36
+ return if klass.abstract?
37
+
38
+ new_name = klass.capability_name
39
+
40
+ # Remove old entry if it exists and points to this class
41
+ if old_name && items[old_name] == klass
42
+ items.delete(old_name)
43
+ end
44
+
45
+ items[new_name] = klass
46
+ klass._registered_name = new_name if klass.respond_to?(:_registered_name=)
25
47
  end
26
48
 
27
49
  # Unregister an item
@@ -5,7 +5,7 @@ module ActionMCP
5
5
  # Base session object that mimics ActiveRecord Session with common functionality
6
6
  class BaseSession
7
7
  attr_accessor :id, :status, :initialized, :role, :messages_count,
8
- :sse_event_counter, :protocol_version, :client_info,
8
+ :protocol_version, :client_info,
9
9
  :client_capabilities, :server_info, :server_capabilities,
10
10
  :tool_registry, :prompt_registry, :resource_registry,
11
11
  :created_at, :updated_at, :ended_at, :last_event_id,
@@ -16,8 +16,6 @@ module ActionMCP
16
16
  @messages = Concurrent::Array.new
17
17
  @subscriptions = Concurrent::Array.new
18
18
  @resources = Concurrent::Array.new
19
- @sse_events = Concurrent::Array.new
20
- @sse_counter = Concurrent::AtomicFixnum.new(0)
21
19
  @message_counter = Concurrent::AtomicFixnum.new(0)
22
20
  @new_record = true
23
21
 
@@ -119,47 +117,6 @@ module ActionMCP
119
117
  ResourceCollection.new(@resources)
120
118
  end
121
119
 
122
- def sse_events
123
- SSEEventCollection.new(@sse_events)
124
- end
125
-
126
- # SSE event management
127
- def increment_sse_counter!
128
- new_value = @sse_counter.increment
129
- self.sse_event_counter = new_value
130
- save
131
- new_value
132
- end
133
-
134
- def store_sse_event(event_id, data, max_events = nil)
135
- max_events ||= max_stored_sse_events
136
- event = { event_id: event_id, data: data, created_at: Time.current }
137
- @sse_events << event
138
-
139
- @sse_events.shift while @sse_events.size > max_events
140
-
141
- event
142
- end
143
-
144
- def get_sse_events_after(last_event_id, limit = 50)
145
- @sse_events.select { |e| e[:event_id] > last_event_id }.first(limit)
146
- end
147
-
148
- def cleanup_old_sse_events(max_age = 15.minutes)
149
- cutoff_time = Time.current - max_age
150
- original_size = @sse_events.size
151
- @sse_events.delete_if { |e| e[:created_at] < cutoff_time }
152
- original_size - @sse_events.size
153
- end
154
-
155
- def max_stored_sse_events
156
- ActionMCP.configuration.max_stored_sse_events || 100
157
- end
158
-
159
- def sse_event_retention_period
160
- ActionMCP.configuration.sse_event_retention_period || 15.minutes
161
- end
162
-
163
120
  # Adapter methods
164
121
  def adapter
165
122
  ActionMCP::Server.server.pubsub
@@ -418,33 +375,6 @@ module ActionMCP
418
375
 
419
376
  class ResourceCollection < Array
420
377
  end
421
-
422
- class SSEEventCollection < Array
423
- def create!(attributes)
424
- self << attributes
425
- attributes
426
- end
427
-
428
- def count
429
- size
430
- end
431
-
432
- def where(_condition, value)
433
- select { |e| e[:event_id] > value }
434
- end
435
-
436
- def order(field)
437
- sort_by { |e| e[field.is_a?(Hash) ? field.keys.first : field] }
438
- end
439
-
440
- def limit(n)
441
- first(n)
442
- end
443
-
444
- def delete_all
445
- clear
446
- end
447
- end
448
378
  end
449
379
  end
450
380
  end
@@ -18,6 +18,8 @@ module ActionMCP
18
18
  @handler.process_resources(rpc_method, id, params)
19
19
  when %r{^tools/}
20
20
  @handler.process_tools(rpc_method, id, params)
21
+ when %r{^tasks/}
22
+ @handler.process_tasks(rpc_method, id, params)
21
23
  when "completion/complete"
22
24
  @handler.process_completion_complete(id, params)
23
25
  else
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ module Handlers
6
+ # Handler for MCP 2025-11-25 Tasks feature
7
+ # Tasks provide durable state machines for tracking async request execution
8
+ module TaskHandler
9
+ include ErrorAware
10
+
11
+ def process_tasks(rpc_method, id, params)
12
+ params ||= {}
13
+
14
+ with_error_handling(id) do
15
+ handler = task_method_handlers[rpc_method]
16
+ if handler
17
+ send(handler, id, params)
18
+ else
19
+ Rails.logger.warn("Unknown tasks method: #{rpc_method}")
20
+ raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Unknown tasks method: #{rpc_method}")
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def task_method_handlers
28
+ {
29
+ JsonRpcHandlerBase::Methods::TASKS_GET => :handle_tasks_get,
30
+ JsonRpcHandlerBase::Methods::TASKS_RESULT => :handle_tasks_result,
31
+ JsonRpcHandlerBase::Methods::TASKS_LIST => :handle_tasks_list,
32
+ JsonRpcHandlerBase::Methods::TASKS_CANCEL => :handle_tasks_cancel,
33
+ JsonRpcHandlerBase::Methods::TASKS_RESUME => :handle_tasks_resume
34
+ }
35
+ end
36
+
37
+ def handle_tasks_get(id, params)
38
+ task_id = validate_required_param(params, "taskId", "Task ID is required")
39
+ task = find_task_or_error(id, task_id)
40
+ return unless task
41
+
42
+ transport.send_tasks_get(id, task_id)
43
+ end
44
+
45
+ def handle_tasks_result(id, params)
46
+ task_id = validate_required_param(params, "taskId", "Task ID is required")
47
+ task = find_task_or_error(id, task_id)
48
+ return unless task
49
+
50
+ transport.send_tasks_result(id, task_id)
51
+ end
52
+
53
+ def handle_tasks_list(id, params)
54
+ cursor = params["cursor"]
55
+ transport.send_tasks_list(id, cursor: cursor)
56
+ end
57
+
58
+ def handle_tasks_cancel(id, params)
59
+ task_id = validate_required_param(params, "taskId", "Task ID is required")
60
+ task = find_task_or_error(id, task_id)
61
+ return unless task
62
+
63
+ transport.send_tasks_cancel(id, task_id)
64
+ end
65
+
66
+ def handle_tasks_resume(id, params)
67
+ task_id = validate_required_param(params, "taskId", "Task ID is required")
68
+ input = params["input"]
69
+ task = find_task_or_error(id, task_id)
70
+ return unless task
71
+
72
+ transport.send_tasks_resume(id, task_id, input)
73
+ end
74
+
75
+ def find_task_or_error(id, task_id)
76
+ task = transport.session.tasks.find_by(id: task_id)
77
+ unless task
78
+ transport.send_jsonrpc_error(id, :invalid_params, "Task '#{task_id}' not found")
79
+ return nil
80
+ end
81
+ task
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -7,6 +7,7 @@ module ActionMCP
7
7
  include Handlers::ToolHandler
8
8
  include Handlers::PromptHandler
9
9
  include Handlers::LoggingHandler
10
+ include Handlers::TaskHandler
10
11
  include ErrorHandling
11
12
  include ErrorAware
12
13
 
@@ -54,6 +55,8 @@ module ActionMCP
54
55
  process_resources(rpc_method, id, params)
55
56
  when %r{^tools/}
56
57
  process_tools(rpc_method, id, params)
58
+ when %r{^tasks/}
59
+ process_tasks(rpc_method, id, params)
57
60
  when Methods::COMPLETION_COMPLETE
58
61
  process_completion_complete(id, params)
59
62
  when Methods::LOGGING_SET_LEVEL
@@ -36,7 +36,7 @@ module ActionMCP
36
36
  # @param success_callback [Proc] Callback for successful subscription
37
37
  # @return [String] Subscription ID
38
38
  def subscribe(channel, message_callback, success_callback = nil)
39
- subscription_id = SecureRandom.uuid
39
+ subscription_id = SecureRandom.uuid_v7
40
40
 
41
41
  @subscriptions[subscription_id] = {
42
42
  channel: channel,
@@ -21,7 +21,7 @@ module ActionMCP
21
21
  # @param success_callback [Proc] Callback for successful subscription
22
22
  # @return [String] Subscription ID
23
23
  def subscribe(channel, message_callback, success_callback = nil)
24
- subscription_id = SecureRandom.uuid
24
+ subscription_id = SecureRandom.uuid_v7
25
25
  session_id = extract_session_id(channel)
26
26
 
27
27
  @subscriptions[subscription_id] = {