actionmcp 0.30.1 → 0.31.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/messages_controller.rb +9 -1
- data/lib/action_mcp/sse_listener.rb +60 -40
- data/lib/action_mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92a0e52de9e401a8e10b8aa49d40cba39769e58f4206c7810face00774e9a5ab
|
4
|
+
data.tar.gz: a0e4fd9420fc44688ea20bb4d4697fbd009594a6d414bafc13563523ed9c5c8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 055db3b1d213b6993694f703bd94a729dc70e2c640580e33828ac8a9193a21302fefadb5de40a89cdde88587114af92966f8675a9e97600fdf53dc01bfd6843b
|
7
|
+
data.tar.gz: 413bc80f29ea5b92b3214a77cae4e42509569b9c5f93b2ce8e10b793797cabad6735fdb4103d96128ca425c1fcf4f04f35cfd5992eaaded1e6262e4f758b0355
|
@@ -21,7 +21,8 @@ module ActionMCP
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def handle_post_message(params, response)
|
24
|
-
|
24
|
+
filtered_params = filter_jsonrpc_params(params)
|
25
|
+
json_rpc_handler.call(filtered_params)
|
25
26
|
response.status = :accepted
|
26
27
|
rescue StandardError => _e
|
27
28
|
response.status = :bad_request
|
@@ -30,5 +31,12 @@ module ActionMCP
|
|
30
31
|
def mcp_session
|
31
32
|
@mcp_session ||= Session.find_or_create_by(id: params[:session_id])
|
32
33
|
end
|
34
|
+
|
35
|
+
def filter_jsonrpc_params(params)
|
36
|
+
# Valid JSON-RPC keys (both request and response)
|
37
|
+
valid_keys = [ "jsonrpc", "method", "params", "id", "result", "error" ]
|
38
|
+
|
39
|
+
params.to_h.slice(*valid_keys)
|
40
|
+
end
|
33
41
|
end
|
34
42
|
end
|
@@ -5,17 +5,14 @@ require "concurrent/promise"
|
|
5
5
|
|
6
6
|
module ActionMCP
|
7
7
|
# Listener class to subscribe to session messages via Action Cable adapter.
|
8
|
-
# Used by controllers handling Server-Sent Events streams.
|
9
8
|
class SSEListener
|
10
|
-
attr_reader :session_key, :adapter
|
11
|
-
|
12
9
|
delegate :session_key, :adapter, to: :@session
|
13
10
|
|
14
11
|
# @param session [ActionMCP::Session]
|
15
12
|
def initialize(session)
|
16
13
|
@session = session
|
17
|
-
@stopped = Concurrent::AtomicBoolean.new
|
18
|
-
@subscription_active = Concurrent::AtomicBoolean.new
|
14
|
+
@stopped = Concurrent::AtomicBoolean.new
|
15
|
+
@subscription_active = Concurrent::AtomicBoolean.new
|
19
16
|
end
|
20
17
|
|
21
18
|
# Start listening using ActionCable's adapter
|
@@ -24,60 +21,83 @@ module ActionMCP
|
|
24
21
|
def start(&callback)
|
25
22
|
Rails.logger.debug "SSEListener: Starting for channel: #{session_key}"
|
26
23
|
|
27
|
-
success_callback =
|
24
|
+
success_callback = -> {
|
28
25
|
Rails.logger.info "SSEListener: Successfully subscribed to channel: #{session_key}"
|
29
26
|
@subscription_active.make_true
|
30
27
|
}
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
message_callback = ->(raw_message) {
|
30
|
+
process_message(raw_message, callback)
|
31
|
+
}
|
32
|
+
|
33
|
+
# Subscribe using the ActionCable adapter
|
34
|
+
adapter.subscribe(session_key, message_callback, success_callback)
|
35
|
+
|
36
|
+
wait_for_subscription
|
37
|
+
end
|
38
|
+
|
39
|
+
# Stops the listener
|
40
|
+
def stop
|
41
|
+
return if @stopped.true?
|
42
|
+
|
43
|
+
@stopped.make_true
|
44
|
+
Rails.logger.debug "SSEListener: Stopping listener for channel: #{session_key}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def process_message(raw_message, callback)
|
50
|
+
return if @stopped.true?
|
35
51
|
|
36
|
-
|
37
|
-
|
52
|
+
begin
|
53
|
+
Rails.logger.debug "SSEListener: Received raw message of type: #{raw_message.class}"
|
54
|
+
|
55
|
+
# Check if the message is a valid JSON string or has a message attribute
|
56
|
+
if raw_message.is_a?(String) && valid_json_format?(raw_message)
|
38
57
|
message = MultiJson.load(raw_message)
|
39
|
-
# Send the message to the callback
|
40
|
-
# TODO: Add SSE event ID here if implementing resumability
|
41
58
|
callback&.call(message)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
59
|
+
elsif raw_message.respond_to?(:message) && raw_message.message.is_a?(String) && valid_json_format?(raw_message.message)
|
60
|
+
message = MultiJson.load(raw_message.message)
|
61
|
+
callback&.call(message)
|
62
|
+
elsif raw_message.respond_to?(:to_json)
|
63
|
+
# Try to serialize the message object to JSON if it responds to to_json
|
64
|
+
message_json = raw_message.to_json
|
65
|
+
if valid_json_format?(message_json)
|
66
|
+
message = MultiJson.load(message_json)
|
67
|
+
callback&.call(message)
|
68
|
+
else
|
69
|
+
Rails.logger.warn "SSEListener: Message cannot be converted to valid JSON"
|
70
|
+
end
|
71
|
+
else
|
72
|
+
# Log that we received an invalid message format
|
73
|
+
display_message = raw_message.to_s[0..100]
|
74
|
+
Rails.logger.warn "SSEListener: Received invalid JSON format: #{display_message}..."
|
46
75
|
end
|
47
|
-
|
76
|
+
rescue StandardError => e
|
77
|
+
Rails.logger.error "SSEListener: Error processing message: #{e.message}"
|
78
|
+
Rails.logger.error "SSEListener: Backtrace: #{e.backtrace.join("\n")}"
|
79
|
+
end
|
80
|
+
end
|
48
81
|
|
49
|
-
|
50
|
-
|
82
|
+
def valid_json_format?(string)
|
83
|
+
return false if string.blank?
|
84
|
+
string = string.strip
|
85
|
+
(string.start_with?("{") && string.end_with?("}")) ||
|
86
|
+
(string.start_with?("[") && string.end_with?("]"))
|
87
|
+
end
|
51
88
|
|
52
|
-
|
89
|
+
def wait_for_subscription
|
53
90
|
subscription_future = Concurrent::Promises.future do
|
54
91
|
sleep 0.1 while !@subscription_active.true? && !@stopped.true?
|
55
92
|
@subscription_active.true?
|
56
93
|
end
|
57
94
|
|
58
|
-
# Wait up to 5 seconds for subscription to be established (increased timeout)
|
59
95
|
begin
|
60
|
-
|
61
|
-
subscription_result || @subscription_active.true?
|
96
|
+
subscription_future.value(5) || @subscription_active.true?
|
62
97
|
rescue Concurrent::TimeoutError
|
63
|
-
Rails.logger.warn "SSEListener: Timed out waiting for subscription
|
98
|
+
Rails.logger.warn "SSEListener: Timed out waiting for subscription for #{session_key}"
|
64
99
|
false
|
65
100
|
end
|
66
101
|
end
|
67
|
-
|
68
|
-
# Stops the listener and attempts to unsubscribe.
|
69
|
-
def stop
|
70
|
-
return if @stopped.true? # Prevent multiple stops
|
71
|
-
|
72
|
-
@stopped.make_true
|
73
|
-
# Unsubscribe using the adapter
|
74
|
-
# Note: ActionCable adapters might not have a direct 'unsubscribe' matching this pattern.
|
75
|
-
# We rely on closing the connection and potentially session cleanup.
|
76
|
-
# If using Redis adapter, explicit unsubscribe might be possible/needed.
|
77
|
-
# For now, just log.
|
78
|
-
Rails.logger.debug "SSEListener: Stopping listener for channel: #{session_key}"
|
79
|
-
# If session cleanup is needed when listener stops, add it here or ensure it happens elsewhere.
|
80
|
-
# Example: @session.close! if @session.role == 'server' # Be careful with side effects
|
81
|
-
end
|
82
102
|
end
|
83
103
|
end
|
data/lib/action_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionmcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.31.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actioncable
|