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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29a0342672e73ed0ff4873d281705ffd06febdd23b9b2d755428b4cd4b74b2fb
4
- data.tar.gz: b1a8a118514b2c41b886f9924b342643c11e3c481bf595211040f7aed8551fa3
3
+ metadata.gz: 92a0e52de9e401a8e10b8aa49d40cba39769e58f4206c7810face00774e9a5ab
4
+ data.tar.gz: a0e4fd9420fc44688ea20bb4d4697fbd009594a6d414bafc13563523ed9c5c8a
5
5
  SHA512:
6
- metadata.gz: 8cdfe964fe67aeae7455b8decae512414ecb8e49610be7f75efd297162098ddbc2f2e5a5d76b6fc55eff9f488dce96aa3ed0c90531e5a263d9071635cf8e46e7
7
- data.tar.gz: b27d59dea26c593800bbcbc7e59007c0e014c6ce751e48d54806033daa57b7081f13175b921f4ddc413e51139dbd807c553fbf325dce045a8e7e743fd6daa7e2
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
- json_rpc_handler.call(params)
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(false)
18
- @subscription_active = Concurrent::AtomicBoolean.new(false)
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 = lambda {
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
- # Set up message callback
33
- message_callback = lambda { |raw_message|
34
- return if @stopped.true?
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
- begin
37
- # Try to parse the message if it's JSON
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
- rescue StandardError => e
43
- Rails.logger.error "SSEListener: Error processing message: #{e.message}"
44
- # Still try to send the raw message as a fallback? Or ignore?
45
- # callback.call(raw_message) if callback
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
- # Subscribe using the ActionCable adapter
50
- adapter.subscribe(session_key, message_callback, success_callback)
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
- # Use a future with timeout to check subscription status
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
- subscription_result = subscription_future.value(5)
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 activation for #{session_key}"
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.30.1"
5
+ VERSION = "0.31.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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.30.1
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 00:00:00.000000000 Z
11
+ date: 2025-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actioncable