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.
- checksums.yaml +4 -4
- data/app/controllers/action_mcp/application_controller.rb +12 -222
- data/app/jobs/action_mcp/tool_execution_job.rb +133 -0
- data/app/models/action_mcp/session/task.rb +204 -0
- data/app/models/action_mcp/session.rb +2 -65
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +2 -0
- data/db/migrate/20251125000001_create_action_mcp_session_tasks.rb +29 -0
- data/db/migrate/20251126000001_add_continuation_state_to_action_mcp_session_tasks.rb +10 -0
- data/db/migrate/20251203000001_remove_sse_support.rb +31 -0
- data/db/migrate/20251204000001_add_progress_to_session_tasks.rb +12 -0
- data/db/test.sqlite3 +0 -0
- data/exe/actionmcp_cli +1 -1
- data/lib/action_mcp/capability.rb +1 -0
- data/lib/action_mcp/client/base.rb +1 -1
- data/lib/action_mcp/configuration.rb +22 -15
- data/lib/action_mcp/engine.rb +8 -1
- data/lib/action_mcp/filtered_logger.rb +0 -3
- data/lib/action_mcp/json_rpc_handler_base.rb +10 -0
- data/lib/action_mcp/prompt.rb +16 -0
- data/lib/action_mcp/registry_base.rb +23 -1
- data/lib/action_mcp/server/base_session.rb +1 -71
- data/lib/action_mcp/server/handlers/router.rb +2 -0
- data/lib/action_mcp/server/handlers/task_handler.rb +86 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +3 -0
- data/lib/action_mcp/server/simple_pub_sub.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +1 -1
- data/lib/action_mcp/server/tasks.rb +125 -0
- data/lib/action_mcp/server/tools.rb +47 -1
- data/lib/action_mcp/server/transport_handler.rb +1 -0
- data/lib/action_mcp/tool.rb +100 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +3 -4
- data/lib/tasks/action_mcp_tasks.rake +1 -35
- metadata +42 -7
- data/app/models/action_mcp/session/sse_event.rb +0 -62
- data/lib/action_mcp/sse_listener.rb +0 -81
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMCP
|
|
4
|
+
module Server
|
|
5
|
+
# Tasks module for MCP 2025-11-25 specification
|
|
6
|
+
# Provides methods for handling task-related requests:
|
|
7
|
+
# - tasks/get: Get task status and data
|
|
8
|
+
# - tasks/result: Get task result (blocking until terminal state)
|
|
9
|
+
# - tasks/list: List tasks for the session
|
|
10
|
+
# - tasks/cancel: Cancel a task
|
|
11
|
+
module Tasks
|
|
12
|
+
# Get task status and metadata
|
|
13
|
+
# @param request_id [String, Integer] JSON-RPC request ID
|
|
14
|
+
# @param task_id [String] Task ID to retrieve
|
|
15
|
+
def send_tasks_get(request_id, task_id)
|
|
16
|
+
task = find_task(task_id)
|
|
17
|
+
return unless task
|
|
18
|
+
|
|
19
|
+
send_jsonrpc_response(request_id, result: { task: task.to_task_data })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get task result, blocking until task reaches terminal state
|
|
23
|
+
# @param request_id [String, Integer] JSON-RPC request ID
|
|
24
|
+
# @param task_id [String] Task ID to get result for
|
|
25
|
+
def send_tasks_result(request_id, task_id)
|
|
26
|
+
task = find_task(task_id)
|
|
27
|
+
return unless task
|
|
28
|
+
|
|
29
|
+
# If task is not in terminal state, wait for it
|
|
30
|
+
# In async execution, client should poll or use SSE for notifications
|
|
31
|
+
unless task.terminal?
|
|
32
|
+
send_jsonrpc_error(request_id, :invalid_request,
|
|
33
|
+
"Task is not yet complete. Current status: #{task.status}")
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
send_jsonrpc_response(request_id, result: task.to_task_result)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# List tasks for the session with optional pagination
|
|
41
|
+
# @param request_id [String, Integer] JSON-RPC request ID
|
|
42
|
+
# @param cursor [String, nil] Pagination cursor
|
|
43
|
+
def send_tasks_list(request_id, cursor: nil)
|
|
44
|
+
# Parse cursor if provided
|
|
45
|
+
offset = cursor.to_i if cursor.present?
|
|
46
|
+
offset ||= 0
|
|
47
|
+
limit = 50
|
|
48
|
+
|
|
49
|
+
tasks = session.tasks.recent.offset(offset).limit(limit + 1)
|
|
50
|
+
has_more = tasks.length > limit
|
|
51
|
+
tasks = tasks.first(limit)
|
|
52
|
+
|
|
53
|
+
result = {
|
|
54
|
+
tasks: tasks.map(&:to_task_data)
|
|
55
|
+
}
|
|
56
|
+
result[:nextCursor] = (offset + limit).to_s if has_more
|
|
57
|
+
|
|
58
|
+
send_jsonrpc_response(request_id, result: result)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Cancel a task
|
|
62
|
+
# @param request_id [String, Integer] JSON-RPC request ID
|
|
63
|
+
# @param task_id [String] Task ID to cancel
|
|
64
|
+
def send_tasks_cancel(request_id, task_id)
|
|
65
|
+
task = find_task(task_id)
|
|
66
|
+
return unless task
|
|
67
|
+
|
|
68
|
+
if task.terminal?
|
|
69
|
+
send_jsonrpc_error(request_id, :invalid_params,
|
|
70
|
+
"Cannot cancel task in terminal status: #{task.status}")
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
task.cancel!
|
|
75
|
+
send_jsonrpc_response(request_id, result: { task: task.to_task_data })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resume a task from input_required state
|
|
79
|
+
# @param request_id [String, Integer] JSON-RPC request ID
|
|
80
|
+
# @param task_id [String] Task ID to resume
|
|
81
|
+
# @param input [Object] Input data for the task
|
|
82
|
+
def send_tasks_resume(request_id, task_id, input)
|
|
83
|
+
task = find_task(task_id)
|
|
84
|
+
return unless task
|
|
85
|
+
|
|
86
|
+
unless task.input_required?
|
|
87
|
+
send_jsonrpc_error(request_id, :invalid_params,
|
|
88
|
+
"Task is not awaiting input. Current status: #{task.status}")
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Store input in continuation state
|
|
93
|
+
continuation = task.continuation_state || {}
|
|
94
|
+
continuation[:input] = input
|
|
95
|
+
task.update!(continuation_state: continuation)
|
|
96
|
+
|
|
97
|
+
# Resume task and re-enqueue job
|
|
98
|
+
task.resume_from_continuation!
|
|
99
|
+
|
|
100
|
+
send_jsonrpc_response(request_id, result: { task: task.to_task_data })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Send task status notification
|
|
104
|
+
# @param task [ActionMCP::Session::Task] Task to notify about
|
|
105
|
+
def send_task_status_notification(task)
|
|
106
|
+
send_jsonrpc_notification(
|
|
107
|
+
JsonRpcHandlerBase::Methods::NOTIFICATIONS_TASKS_STATUS,
|
|
108
|
+
{ task: task.to_task_data }
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def find_task(task_id)
|
|
115
|
+
task = session.tasks.find_by(id: task_id)
|
|
116
|
+
unless task
|
|
117
|
+
Rails.logger.warn "Task not found: #{task_id}"
|
|
118
|
+
# Note: we need the request_id to send error, but this is called from handler
|
|
119
|
+
# The handler should handle the nil return
|
|
120
|
+
end
|
|
121
|
+
task
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -58,6 +58,20 @@ module ActionMCP
|
|
|
58
58
|
return
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Check for task-augmented execution (MCP 2025-11-25)
|
|
62
|
+
task_params = _meta["task"] || _meta[:task]
|
|
63
|
+
if task_params && tasks_enabled?
|
|
64
|
+
handle_task_augmented_tool_call(request_id, tool_name, arguments, _meta, task_params)
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Standard synchronous execution
|
|
69
|
+
execute_tool_synchronously(request_id, tool_class, tool_name, arguments, _meta)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def execute_tool_synchronously(request_id, tool_class, tool_name, arguments, _meta)
|
|
61
75
|
begin
|
|
62
76
|
# Create tool and set execution context with request info
|
|
63
77
|
tool = tool_class.new(arguments)
|
|
@@ -106,7 +120,39 @@ module ActionMCP
|
|
|
106
120
|
end
|
|
107
121
|
end
|
|
108
122
|
|
|
109
|
-
|
|
123
|
+
# Handle task-augmented tool calls per MCP 2025-11-25 specification
|
|
124
|
+
# Creates a Task record and executes the tool asynchronously
|
|
125
|
+
def handle_task_augmented_tool_call(request_id, tool_name, arguments, _meta, task_params)
|
|
126
|
+
# Extract task configuration
|
|
127
|
+
ttl = task_params["ttl"] || task_params[:ttl] || 60_000
|
|
128
|
+
poll_interval = task_params["pollInterval"] || task_params[:pollInterval] || 5_000
|
|
129
|
+
|
|
130
|
+
# Create task record
|
|
131
|
+
task = session.tasks.create!(
|
|
132
|
+
request_method: "tools/call",
|
|
133
|
+
request_name: tool_name,
|
|
134
|
+
request_params: {
|
|
135
|
+
name: tool_name,
|
|
136
|
+
arguments: arguments,
|
|
137
|
+
_meta: _meta
|
|
138
|
+
},
|
|
139
|
+
ttl: ttl,
|
|
140
|
+
poll_interval: poll_interval
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Return CreateTaskResult immediately
|
|
144
|
+
send_jsonrpc_response(request_id, result: { task: task.to_task_data })
|
|
145
|
+
|
|
146
|
+
# Execute tool asynchronously via ActiveJob
|
|
147
|
+
ToolExecutionJob.perform_later(task.id, tool_name, arguments, _meta)
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
Rails.logger.error "Failed to create task: #{e.class} - #{e.message}"
|
|
150
|
+
send_jsonrpc_error(request_id, :internal_error, "Failed to create task")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def tasks_enabled?
|
|
154
|
+
ActionMCP.configuration.tasks_enabled
|
|
155
|
+
end
|
|
110
156
|
|
|
111
157
|
def format_registry_items(registry, protocol_version = nil)
|
|
112
158
|
registry.map { |item| item.klass.to_h(protocol_version: protocol_version) }
|
data/lib/action_mcp/tool.rb
CHANGED
|
@@ -29,6 +29,8 @@ module ActionMCP
|
|
|
29
29
|
class_attribute :_output_schema_builder, instance_accessor: false, default: nil
|
|
30
30
|
class_attribute :_additional_properties, instance_accessor: false, default: nil
|
|
31
31
|
class_attribute :_cached_schema_property_keys, instance_accessor: false, default: nil
|
|
32
|
+
class_attribute :_task_support, instance_accessor: false, default: :forbidden
|
|
33
|
+
class_attribute :_resumable_steps_block, instance_accessor: false, default: nil
|
|
32
34
|
|
|
33
35
|
# --------------------------------------------------------------------------
|
|
34
36
|
# Tool Name and Description DSL
|
|
@@ -40,11 +42,27 @@ module ActionMCP
|
|
|
40
42
|
def self.tool_name(name = nil)
|
|
41
43
|
if name
|
|
42
44
|
self._capability_name = name
|
|
45
|
+
re_register_if_needed
|
|
46
|
+
name
|
|
43
47
|
else
|
|
44
48
|
_capability_name || default_tool_name
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
# Re-registers the tool if it was already registered under a different name
|
|
53
|
+
# @return [void]
|
|
54
|
+
def self.re_register_if_needed
|
|
55
|
+
return if abstract?
|
|
56
|
+
return unless _registered_name # Not yet registered
|
|
57
|
+
|
|
58
|
+
new_name = capability_name
|
|
59
|
+
return if _registered_name == new_name # No change
|
|
60
|
+
|
|
61
|
+
ActionMCP::ToolsRegistry.re_register(self, _registered_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private_class_method :re_register_if_needed
|
|
65
|
+
|
|
48
66
|
# Returns a default tool name based on the class name.
|
|
49
67
|
#
|
|
50
68
|
# @return [String] The default tool name.
|
|
@@ -178,6 +196,60 @@ module ActionMCP
|
|
|
178
196
|
_requires_consent
|
|
179
197
|
end
|
|
180
198
|
|
|
199
|
+
# --------------------------------------------------------------------------
|
|
200
|
+
# Task Support DSL (MCP 2025-11-25)
|
|
201
|
+
# --------------------------------------------------------------------------
|
|
202
|
+
# Sets or retrieves the task support mode for this tool
|
|
203
|
+
# @param mode [Symbol, nil] :required, :optional, or :forbidden (default)
|
|
204
|
+
# @return [Symbol] The current task support mode
|
|
205
|
+
def task_support(mode = nil)
|
|
206
|
+
if mode
|
|
207
|
+
unless %i[required optional forbidden].include?(mode)
|
|
208
|
+
raise ArgumentError, "task_support must be :required, :optional, or :forbidden"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
self._task_support = mode
|
|
212
|
+
else
|
|
213
|
+
_task_support
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Convenience methods for task support
|
|
218
|
+
def task_required!
|
|
219
|
+
self._task_support = :required
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def task_optional!
|
|
223
|
+
self._task_support = :optional
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def task_forbidden!
|
|
227
|
+
self._task_support = :forbidden
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Returns the execution metadata including task support
|
|
231
|
+
def execution_metadata
|
|
232
|
+
{
|
|
233
|
+
taskSupport: _task_support.to_s
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# --------------------------------------------------------------------------
|
|
238
|
+
# Resumable Steps DSL (ActiveJob::Continuable support)
|
|
239
|
+
# --------------------------------------------------------------------------
|
|
240
|
+
# Defines resumable execution steps for long-running tools
|
|
241
|
+
# @param block [Proc] Block containing step definitions
|
|
242
|
+
# @return [void]
|
|
243
|
+
def resumable_steps(&block)
|
|
244
|
+
self._resumable_steps_block = block
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Checks if tool has resumable steps defined
|
|
248
|
+
# @return [Boolean]
|
|
249
|
+
def resumable_steps_defined?
|
|
250
|
+
_resumable_steps_block.present?
|
|
251
|
+
end
|
|
252
|
+
|
|
181
253
|
# Sets or retrieves the additionalProperties setting for the input schema
|
|
182
254
|
# @param enabled [Boolean, Hash] true to allow any additional properties,
|
|
183
255
|
# false to disallow them, or a Hash for typed additional properties
|
|
@@ -322,6 +394,12 @@ module ActionMCP
|
|
|
322
394
|
annotations = annotations_for_protocol(protocol_version)
|
|
323
395
|
result[:annotations] = annotations if annotations.any?
|
|
324
396
|
|
|
397
|
+
# Add execution metadata (MCP 2025-11-25)
|
|
398
|
+
# Only include if not default (forbidden) to minimize payload
|
|
399
|
+
if _task_support && _task_support != :forbidden
|
|
400
|
+
result[:execution] = execution_metadata
|
|
401
|
+
end
|
|
402
|
+
|
|
325
403
|
# Add _meta if present
|
|
326
404
|
result[:_meta] = _meta if _meta.any?
|
|
327
405
|
|
|
@@ -438,6 +516,28 @@ module ActionMCP
|
|
|
438
516
|
raise NotImplementedError, "Subclasses must implement the perform method"
|
|
439
517
|
end
|
|
440
518
|
|
|
519
|
+
# Request user/client input and pause execution
|
|
520
|
+
# @param prompt [String] The prompt/question for the user
|
|
521
|
+
# @param context [Hash] Additional context about the input request
|
|
522
|
+
# @return [void]
|
|
523
|
+
def request_input!(prompt:, context: {})
|
|
524
|
+
@_input_required = { prompt: prompt, context: context }
|
|
525
|
+
throw :halt_execution # Use throw to exit perform block
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Report progress for long-running tool operations
|
|
529
|
+
# Only available when tool is running in task-augmented mode
|
|
530
|
+
# @param percent [Integer] Progress percentage (0-100)
|
|
531
|
+
# @param message [String] Optional progress message
|
|
532
|
+
# @param cursor [Integer, String] Optional cursor for iteration state
|
|
533
|
+
# @return [void]
|
|
534
|
+
def report_progress!(percent:, message: nil, cursor: nil)
|
|
535
|
+
return unless @_task # Only available in task-augmented mode
|
|
536
|
+
|
|
537
|
+
@_task.update_progress!(percent: percent, message: message)
|
|
538
|
+
@_task.record_step!(:in_progress, cursor: cursor) if cursor
|
|
539
|
+
end
|
|
540
|
+
|
|
441
541
|
private
|
|
442
542
|
|
|
443
543
|
# Helper method for tools to manually report errors
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
|
@@ -25,7 +25,6 @@ Zeitwerk::Loader.for_gem.tap do |loader|
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
loader.inflector.inflect("action_mcp" => "ActionMCP")
|
|
28
|
-
loader.inflector.inflect("sse_listener" => "SSEListener")
|
|
29
28
|
end.setup
|
|
30
29
|
|
|
31
30
|
module ActionMCP
|
|
@@ -34,12 +33,12 @@ module ActionMCP
|
|
|
34
33
|
|
|
35
34
|
# Protocol version constants
|
|
36
35
|
SUPPORTED_VERSIONS = [
|
|
37
|
-
"2025-
|
|
38
|
-
"2025-
|
|
36
|
+
"2025-11-25", # The Task Master - Tasks, icons, tool naming, polling SSE
|
|
37
|
+
"2025-06-18" # Dr. Identity McBouncer - elicitation, structured output, resource links
|
|
39
38
|
].freeze
|
|
40
39
|
|
|
41
40
|
LATEST_VERSION = SUPPORTED_VERSIONS.first.freeze
|
|
42
|
-
DEFAULT_PROTOCOL_VERSION = "2025-
|
|
41
|
+
DEFAULT_PROTOCOL_VERSION = "2025-06-18" # Default to previous stable version for backwards compatibility
|
|
43
42
|
class << self
|
|
44
43
|
# Returns a Rack-compatible application for serving MCP requests
|
|
45
44
|
# This makes ActionMCP.server work similar to ActionCable.server
|
|
@@ -179,10 +179,7 @@ namespace :action_mcp do
|
|
|
179
179
|
|
|
180
180
|
# Transport Configuration
|
|
181
181
|
puts "\n\e[36mTransport Configuration:\e[0m"
|
|
182
|
-
puts "
|
|
183
|
-
puts " Post Response Preference: #{config.post_response_preference}"
|
|
184
|
-
puts " SSE Event Retention Period: #{config.sse_event_retention_period}"
|
|
185
|
-
puts " Max Stored SSE Events: #{config.max_stored_sse_events}"
|
|
182
|
+
puts " Protocol Version: #{config.protocol_version}"
|
|
186
183
|
|
|
187
184
|
# Pub/Sub Adapter
|
|
188
185
|
puts "\n\e[36mPub/Sub Adapter:\e[0m"
|
|
@@ -332,37 +329,6 @@ namespace :action_mcp do
|
|
|
332
329
|
puts " Error accessing message data: #{e.message}"
|
|
333
330
|
end
|
|
334
331
|
|
|
335
|
-
# SSE Event Statistics (if table exists)
|
|
336
|
-
puts "\n\e[36mSSE Event Statistics:\e[0m"
|
|
337
|
-
|
|
338
|
-
begin
|
|
339
|
-
if ActionMCP::ApplicationRecord.connection.table_exists?("action_mcp_sse_events")
|
|
340
|
-
total_events = ActionMCP::Session::SSEEvent.count
|
|
341
|
-
puts " Total SSE Events: #{total_events}"
|
|
342
|
-
|
|
343
|
-
if total_events.positive?
|
|
344
|
-
# Recent events
|
|
345
|
-
recent_events = ActionMCP::Session::SSEEvent.where("created_at > ?", 1.hour.ago).count
|
|
346
|
-
puts " SSE Events (Last Hour): #{recent_events}"
|
|
347
|
-
|
|
348
|
-
# Events by session
|
|
349
|
-
events_by_session = ActionMCP::Session::SSEEvent.joins(:session)
|
|
350
|
-
.group("action_mcp_sessions.id")
|
|
351
|
-
.count
|
|
352
|
-
.sort_by { |_session_id, count| -count }
|
|
353
|
-
.first(5)
|
|
354
|
-
puts " Top Sessions by SSE Events:"
|
|
355
|
-
events_by_session.each do |session_id, count|
|
|
356
|
-
puts " #{session_id}: #{count} events"
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
else
|
|
360
|
-
puts " SSE Events table not found"
|
|
361
|
-
end
|
|
362
|
-
rescue StandardError => e
|
|
363
|
-
puts " Error accessing SSE event data: #{e.message}"
|
|
364
|
-
end
|
|
365
|
-
|
|
366
332
|
# Storage Information
|
|
367
333
|
puts "\n\e[36mStorage Information:\e[0m"
|
|
368
334
|
puts " Session Store Type: #{ActionMCP.configuration.session_store_type}"
|
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.100.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -9,20 +9,34 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activejob
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.1.0
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: activerecord
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - ">="
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 8.0
|
|
32
|
+
version: 8.1.0
|
|
19
33
|
type: :runtime
|
|
20
34
|
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - ">="
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 8.0
|
|
39
|
+
version: 8.1.0
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: concurrent-ruby
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,14 +85,14 @@ dependencies:
|
|
|
71
85
|
requirements:
|
|
72
86
|
- - ">="
|
|
73
87
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 8.0
|
|
88
|
+
version: 8.1.0
|
|
75
89
|
type: :runtime
|
|
76
90
|
prerelease: false
|
|
77
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
92
|
requirements:
|
|
79
93
|
- - ">="
|
|
80
94
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 8.0
|
|
95
|
+
version: 8.1.0
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: zeitwerk
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +107,20 @@ dependencies:
|
|
|
93
107
|
- - "~>"
|
|
94
108
|
- !ruby/object:Gem::Version
|
|
95
109
|
version: '2.6'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: state_machines-activerecord
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: 0.100.0
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: 0.100.0
|
|
96
124
|
- !ruby/object:Gem::Dependency
|
|
97
125
|
name: json_schemer
|
|
98
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -121,19 +149,25 @@ files:
|
|
|
121
149
|
- README.md
|
|
122
150
|
- Rakefile
|
|
123
151
|
- app/controllers/action_mcp/application_controller.rb
|
|
152
|
+
- app/jobs/action_mcp/tool_execution_job.rb
|
|
124
153
|
- app/models/action_mcp.rb
|
|
125
154
|
- app/models/action_mcp/application_record.rb
|
|
126
155
|
- app/models/action_mcp/session.rb
|
|
127
156
|
- app/models/action_mcp/session/message.rb
|
|
128
157
|
- app/models/action_mcp/session/resource.rb
|
|
129
|
-
- app/models/action_mcp/session/sse_event.rb
|
|
130
158
|
- app/models/action_mcp/session/subscription.rb
|
|
159
|
+
- app/models/action_mcp/session/task.rb
|
|
131
160
|
- app/models/concerns/action_mcp/mcp_console_helpers.rb
|
|
132
161
|
- app/models/concerns/action_mcp/mcp_message_inspect.rb
|
|
133
162
|
- config/routes.rb
|
|
134
163
|
- db/migrate/20250512154359_consolidated_migration.rb
|
|
135
164
|
- db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb
|
|
136
165
|
- db/migrate/20250727000001_remove_oauth_support.rb
|
|
166
|
+
- db/migrate/20251125000001_create_action_mcp_session_tasks.rb
|
|
167
|
+
- db/migrate/20251126000001_add_continuation_state_to_action_mcp_session_tasks.rb
|
|
168
|
+
- db/migrate/20251203000001_remove_sse_support.rb
|
|
169
|
+
- db/migrate/20251204000001_add_progress_to_session_tasks.rb
|
|
170
|
+
- db/test.sqlite3
|
|
137
171
|
- exe/actionmcp_cli
|
|
138
172
|
- lib/action_mcp.rb
|
|
139
173
|
- lib/action_mcp/base_response.rb
|
|
@@ -223,6 +257,7 @@ files:
|
|
|
223
257
|
- lib/action_mcp/server/handlers/prompt_handler.rb
|
|
224
258
|
- lib/action_mcp/server/handlers/resource_handler.rb
|
|
225
259
|
- lib/action_mcp/server/handlers/router.rb
|
|
260
|
+
- lib/action_mcp/server/handlers/task_handler.rb
|
|
226
261
|
- lib/action_mcp/server/handlers/tool_handler.rb
|
|
227
262
|
- lib/action_mcp/server/json_rpc_handler.rb
|
|
228
263
|
- lib/action_mcp/server/messaging_service.rb
|
|
@@ -237,11 +272,11 @@ files:
|
|
|
237
272
|
- lib/action_mcp/server/session_store_factory.rb
|
|
238
273
|
- lib/action_mcp/server/simple_pub_sub.rb
|
|
239
274
|
- lib/action_mcp/server/solid_mcp_adapter.rb
|
|
275
|
+
- lib/action_mcp/server/tasks.rb
|
|
240
276
|
- lib/action_mcp/server/test_session_store.rb
|
|
241
277
|
- lib/action_mcp/server/tools.rb
|
|
242
278
|
- lib/action_mcp/server/transport_handler.rb
|
|
243
279
|
- lib/action_mcp/server/volatile_session_store.rb
|
|
244
|
-
- lib/action_mcp/sse_listener.rb
|
|
245
280
|
- lib/action_mcp/string_array.rb
|
|
246
281
|
- lib/action_mcp/tagged_stream_logging.rb
|
|
247
282
|
- lib/action_mcp/test_helper.rb
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# <rails-lens:schema:begin>
|
|
4
|
-
# table = "action_mcp_sse_events"
|
|
5
|
-
# database_dialect = "SQLite"
|
|
6
|
-
#
|
|
7
|
-
# columns = [
|
|
8
|
-
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
|
9
|
-
# { name = "session_id", type = "string", nullable = false },
|
|
10
|
-
# { name = "event_id", type = "integer", nullable = false },
|
|
11
|
-
# { name = "data", type = "text", nullable = false },
|
|
12
|
-
# { name = "created_at", type = "datetime", nullable = false },
|
|
13
|
-
# { name = "updated_at", type = "datetime", nullable = false }
|
|
14
|
-
# ]
|
|
15
|
-
#
|
|
16
|
-
# indexes = [
|
|
17
|
-
# { name = "index_action_mcp_sse_events_on_created_at", columns = ["created_at"] },
|
|
18
|
-
# { name = "index_action_mcp_sse_events_on_session_id_and_event_id", columns = ["session_id", "event_id"], unique = true },
|
|
19
|
-
# { name = "index_action_mcp_sse_events_on_session_id", columns = ["session_id"] }
|
|
20
|
-
# ]
|
|
21
|
-
#
|
|
22
|
-
# foreign_keys = [
|
|
23
|
-
# { column = "session_id", references_table = "action_mcp_sessions", references_column = "id" }
|
|
24
|
-
# ]
|
|
25
|
-
#
|
|
26
|
-
# == Notes
|
|
27
|
-
# - Association 'session' should specify inverse_of
|
|
28
|
-
# - String column 'session_id' has no length limit - consider adding one
|
|
29
|
-
# <rails-lens:schema:end>
|
|
30
|
-
module ActionMCP
|
|
31
|
-
class Session
|
|
32
|
-
# Represents a Server-Sent Event (SSE) in an MCP session
|
|
33
|
-
# These events are stored for potential resumption when a client reconnects
|
|
34
|
-
class SSEEvent < ApplicationRecord
|
|
35
|
-
self.table_name = "action_mcp_sse_events"
|
|
36
|
-
|
|
37
|
-
belongs_to :session, class_name: "ActionMCP::Session"
|
|
38
|
-
|
|
39
|
-
# Validations
|
|
40
|
-
validates :event_id, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
|
41
|
-
validates :data, presence: true
|
|
42
|
-
|
|
43
|
-
# Scopes
|
|
44
|
-
scope :recent, -> { order(event_id: :desc) }
|
|
45
|
-
scope :after_id, ->(id) { where("event_id > ?", id) }
|
|
46
|
-
scope :before, ->(time) { where("created_at < ?", time) }
|
|
47
|
-
|
|
48
|
-
# Serializes the data as JSON if it's not already a string
|
|
49
|
-
def data_for_stream
|
|
50
|
-
return data if data.is_a?(String)
|
|
51
|
-
|
|
52
|
-
data.is_a?(Hash) ? data.to_json : data.to_s
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Generates the SSE formatted event string
|
|
56
|
-
# @return [String] The formatted SSE event
|
|
57
|
-
def to_sse
|
|
58
|
-
"id: #{event_id}\ndata: #{data_for_stream}\n\n"
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "concurrent/atomic/atomic_boolean"
|
|
4
|
-
require "concurrent/promise"
|
|
5
|
-
|
|
6
|
-
module ActionMCP
|
|
7
|
-
# Listener class to subscribe to session messages via PubSub adapter.
|
|
8
|
-
class SSEListener
|
|
9
|
-
delegate :session_key, :adapter, to: :@session
|
|
10
|
-
|
|
11
|
-
# @param session [ActionMCP::Session]
|
|
12
|
-
def initialize(session)
|
|
13
|
-
@session = session
|
|
14
|
-
@stopped = Concurrent::AtomicBoolean.new
|
|
15
|
-
@subscription_active = Concurrent::AtomicBoolean.new
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Start listening using PubSub adapter
|
|
19
|
-
# @yield [Hash] Yields parsed message received from the pub/sub channel
|
|
20
|
-
# @return [Boolean] True if subscription was successful within timeout, false otherwise.
|
|
21
|
-
def start(&callback)
|
|
22
|
-
success_callback = lambda {
|
|
23
|
-
@subscription_active.make_true
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
message_callback = lambda { |raw_message|
|
|
27
|
-
process_message(raw_message, callback)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
# Subscribe using the PubSub adapter
|
|
31
|
-
adapter.subscribe(session_key, message_callback, success_callback)
|
|
32
|
-
|
|
33
|
-
wait_for_subscription
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Stops the listener
|
|
37
|
-
def stop
|
|
38
|
-
return if @stopped.true?
|
|
39
|
-
|
|
40
|
-
@stopped.make_true
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def process_message(raw_message, callback)
|
|
46
|
-
return if @stopped.true?
|
|
47
|
-
|
|
48
|
-
begin
|
|
49
|
-
# Check if the message is a valid JSON string or has a message attribute
|
|
50
|
-
if raw_message.is_a?(String) && valid_json_format?(raw_message)
|
|
51
|
-
message = MultiJson.load(raw_message)
|
|
52
|
-
callback&.call(message)
|
|
53
|
-
end
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
Rails.logger.error "SSEListener: Error processing message: #{e.message}"
|
|
56
|
-
Rails.logger.error "SSEListener: Backtrace: #{e.backtrace.join("\n")}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def valid_json_format?(string)
|
|
61
|
-
return false if string.blank?
|
|
62
|
-
|
|
63
|
-
string = string.strip
|
|
64
|
-
(string.start_with?("{") && string.end_with?("}")) ||
|
|
65
|
-
(string.start_with?("[") && string.end_with?("]"))
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def wait_for_subscription
|
|
69
|
-
subscription_future = Concurrent::Promises.future do
|
|
70
|
-
sleep 0.1 while !@subscription_active.true? && !@stopped.true?
|
|
71
|
-
@subscription_active.true?
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
begin
|
|
75
|
-
subscription_future.value(5) || @subscription_active.true?
|
|
76
|
-
rescue Concurrent::TimeoutError
|
|
77
|
-
false
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|