actionmcp 0.30.0 → 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
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
|
@@ -58,13 +58,10 @@ module ActionMCP
|
|
58
58
|
|
59
59
|
# @param payload [String, Hash]
|
60
60
|
def data=(payload)
|
61
|
-
@data = payload
|
62
|
-
|
63
61
|
# Convert string payloads to JSON
|
64
62
|
if payload.is_a?(String)
|
65
63
|
begin
|
66
|
-
|
67
|
-
self.message_json = parsed_json
|
64
|
+
@data = MultiJson.load(payload)
|
68
65
|
rescue MultiJson::ParseError
|
69
66
|
# Handle invalid JSON by creating an error object
|
70
67
|
self.message_json = { "error" => "Invalid JSON", "raw" => payload }
|
@@ -72,10 +69,12 @@ module ActionMCP
|
|
72
69
|
return
|
73
70
|
end
|
74
71
|
else
|
75
|
-
#
|
76
|
-
|
72
|
+
# If it's already a hash/array, use it directly
|
73
|
+
@data = payload
|
77
74
|
end
|
78
|
-
|
75
|
+
|
76
|
+
self.message_json = @data
|
77
|
+
process_json_content(@data)
|
79
78
|
end
|
80
79
|
|
81
80
|
def data
|
@@ -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,13 +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
|
+
autorequire:
|
8
9
|
bindir: exe
|
9
10
|
cert_chain: []
|
10
|
-
date: 2025-
|
11
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: actioncable
|
@@ -211,6 +212,7 @@ metadata:
|
|
211
212
|
source_code_uri: https://github.com/seuros/action_mcp
|
212
213
|
changelog_uri: https://github.com/seuros/action_mcp/blob/master/CHANGELOG.md
|
213
214
|
rubygems_mfa_required: 'true'
|
215
|
+
post_install_message:
|
214
216
|
rdoc_options: []
|
215
217
|
require_paths:
|
216
218
|
- lib
|
@@ -225,7 +227,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
225
227
|
- !ruby/object:Gem::Version
|
226
228
|
version: '0'
|
227
229
|
requirements: []
|
228
|
-
rubygems_version: 3.
|
230
|
+
rubygems_version: 3.5.22
|
231
|
+
signing_key:
|
229
232
|
specification_version: 4
|
230
233
|
summary: Provides essential tooling for building Model Context Protocol (MCP) capable
|
231
234
|
servers
|