actionmcp 0.2.3 → 0.2.4

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: 51df9fa26245d233eebbbf193729618ee8c0f8cc7aa6c680d1a2daac97b9a19e
4
- data.tar.gz: b3b898d71781a538ee1826c64294018494e4824a0971768adadcb516be2dd2df
3
+ metadata.gz: 0a1cd6d70ca62ff8ecf4d25c00e29d9592bc9d80ca46db50330a2f818a66ce1f
4
+ data.tar.gz: 259e35b08f9627aa4165157b71436f4fdd4c0c9eaedc2ae41ddb6f0bb8c3fdde
5
5
  SHA512:
6
- metadata.gz: a5304f933ec4c0b4e97ca263c2c6833a380dd99ae3beca60e00709c71dbe921024e9572bf1a2ba38e69729d8c8d7728c6d1ab2d3e854cdbfc720c8c494fb59fc
7
- data.tar.gz: c79eac91747d34080ae55326d41005e0a71e2ab940abbb858f35e6601797e6ea923f0e4a21bf718456a0a3a6688a05c833ec41e5717d03bd6fcf4eaff2aa554b
6
+ metadata.gz: c213f50edbec5dc96af78b3090f562dae9fe479a405a44d573927ef65c1c042d6a43ef174297bca3bc92280a372d5a3a74c61aec5a8dfc3e1381550c56b27397
7
+ data.tar.gz: 343bee907d44e4e7869372b5ffa250c6d628c8789ef51a4690b311c494d6b5b20199837dbbc5f39959e36b8a71d3ce2942b82685b4076897ddc393e59e73d8e3
@@ -0,0 +1,13 @@
1
+ module ActionMCP
2
+ class ApplicationController < ActionController::Metal
3
+ abstract!
4
+ ActionController::API.without_modules(:StrongParameters, :ParamsWrapper).each do |left|
5
+ include left
6
+ end
7
+ include Engine.routes.url_helpers
8
+
9
+ def session_key
10
+ @session_key = "action_mcp-sessions-#{session_id}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ module ActionMCP
2
+ class MessagesController < ApplicationController
3
+ # @route POST / (sse_in)
4
+ def create
5
+ begin
6
+ handle_post_message(params, response)
7
+ rescue => e
8
+ head :internal_server_error
9
+ end
10
+ head response.status
11
+ end
12
+
13
+ private
14
+
15
+ def transport
16
+ @transport ||= Transport.new(session_key)
17
+ end
18
+
19
+ def transport_handler
20
+ TransportHandler.new(transport)
21
+ end
22
+
23
+ def json_rpc_handler
24
+ @json_rpc_handler ||= ActionMCP::JsonRpcHandler.new(transport_handler)
25
+ end
26
+
27
+ def handle_post_message(params, response)
28
+ json_rpc_handler.call(params)
29
+
30
+ response.status = :accepted
31
+ rescue StandardError => e
32
+ response.status = :bad_request
33
+ end
34
+
35
+ def session_id
36
+ params[:session_id]
37
+ end
38
+
39
+ class Transport
40
+ attr_reader :session_key, :adapter
41
+ def initialize(session_key)
42
+ @session_key = session_key
43
+ @adapter = ActionMCP::Server.server.pubsub
44
+ end
45
+
46
+ def write(data)
47
+ adapter.broadcast(session_key, data.to_json)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,151 @@
1
+ module ActionMCP
2
+ class SSEController < ApplicationController
3
+ HEARTBEAT_INTERVAL = 10
4
+ INITIALIZATION_TIMEOUT = 2
5
+ include ActionController::Live
6
+
7
+ # @route GET /sse (sse_out)
8
+ def events
9
+ # Set headers first
10
+ response.headers["X-Accel-Buffering"] = "no"
11
+ response.headers["Content-Type"] = "text/event-stream"
12
+ response.headers["Cache-Control"] = "no-cache"
13
+ response.headers["Connection"] = "keep-alive"
14
+
15
+ listener = nil
16
+ begin
17
+ # Now start streaming - send endpoint
18
+ send_endpoint_event(sse_in_url)
19
+
20
+ # Start listener and process messages via the transport
21
+ listener = SseListener.new(session_key)
22
+ if listener.start do |message|
23
+ begin
24
+ Rails.logger.debug "Processing message in controller: #{message.inspect} (#{message.class})"
25
+
26
+ # Send with proper SSE formatting
27
+ sse = SSE.new(response.stream)
28
+ sse.write(message)
29
+ rescue => e
30
+ Rails.logger.error "Error sending SSE message: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
31
+ end
32
+ end
33
+
34
+ # Heartbeat loop
35
+ until response.stream.closed?
36
+ sleep HEARTBEAT_INTERVAL
37
+ send_ping!
38
+ end
39
+ else
40
+ Rails.logger.error "Listener failed to activate for session: #{session_id}"
41
+ raise "Failed to establish subscription"
42
+ end
43
+ rescue ActionController::Live::ClientDisconnected, IOError => e
44
+ Rails.logger.debug "SSE: Expected disconnection: #{e.message}"
45
+ rescue => e
46
+ Rails.logger.error "SSE: Unexpected error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
47
+ ensure
48
+ listener&.stop
49
+ response.stream.close
50
+ Rails.logger.debug "SSE: Connection closed for session: #{session_id}"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def send_endpoint_event(messages_url)
57
+ endpoint = "#{messages_url}?session_id=#{session_id}"
58
+ SSE.new(response.stream,
59
+ event: "endpoint")
60
+ .write(endpoint)
61
+ end
62
+
63
+ def send_ping!
64
+ SSE.new(response.stream,
65
+ event: "ping")
66
+ .write(Time.now.to_i)
67
+ end
68
+
69
+ def default_url_options
70
+ { host: request.host, port: request.port }
71
+ end
72
+
73
+ def session_id
74
+ @session_id ||= SecureRandom.hex(6)
75
+ end
76
+ end
77
+
78
+ class SseListener
79
+ attr_reader :session_key, :adapter
80
+
81
+ def initialize(session_key)
82
+ @session_key = session_key
83
+ @adapter = ActionMCP::Server.server.pubsub
84
+ @stopped = false
85
+ @subscription_active = false
86
+ end
87
+
88
+ # Start listening using ActionCable's PostgreSQL adapter
89
+ def start(&callback)
90
+ Rails.logger.debug "Starting listener for channel: #{session_key}"
91
+
92
+ # Set up success callback
93
+ success_callback = -> {
94
+ Rails.logger.debug "Successfully subscribed to channel: #{session_key}"
95
+ @subscription_active = true
96
+ }
97
+
98
+ # Set up message callback with detailed debugging
99
+ message_callback = ->(raw_message) {
100
+ Rails.logger.debug "Received raw message via adapter: #{raw_message.inspect} (#{raw_message.class})"
101
+
102
+ begin
103
+ # Try to parse the message if it's JSON
104
+ message = raw_message.is_a?(String) ? JSON.parse(raw_message) : raw_message
105
+ Rails.logger.debug "Processed message: #{message.inspect}"
106
+
107
+ # Send the message to the callback
108
+ callback.call(message) if callback && !@stopped
109
+ rescue => e
110
+ Rails.logger.error "Error processing message: #{e.class} - #{e.message}"
111
+ # Still try to send the raw message as a fallback
112
+ callback.call(raw_message) if callback && !@stopped
113
+ end
114
+ }
115
+
116
+ # Subscribe using the ActionCable adapter
117
+ adapter.subscribe(session_key, message_callback, success_callback)
118
+
119
+ # Give some time for the subscription to be established
120
+ sleep 1.5
121
+
122
+ # Check if subscription was successful
123
+ if @subscription_active
124
+ Rails.logger.debug "Subscription confirmed active for: #{session_key}"
125
+ true
126
+ else
127
+ Rails.logger.error "Failed to activate subscription for: #{session_key}"
128
+ false
129
+ end
130
+ end
131
+
132
+ def stop
133
+ Rails.logger.debug "Stopping listener for: #{session_key}"
134
+ @stopped = true
135
+
136
+ # Unsubscribe using the correct method signature
137
+ begin
138
+ # Create a dummy callback that matches the one we provided in start
139
+ dummy_callback = ->(_) { }
140
+ adapter.unsubscribe(session_key, dummy_callback)
141
+ Rails.logger.debug "Unsubscribed from: #{session_key}"
142
+ rescue => e
143
+ Rails.logger.error "Error unsubscribing from #{session_key}: #{e.message}"
144
+ end
145
+ end
146
+
147
+ def active?
148
+ @subscription_active
149
+ end
150
+ end
151
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ ActionMCP::Engine.routes.draw do
2
+ get "/", to: "sse#events", as: :sse_out
3
+ post "/", to: "messages#create", as: :sse_in
4
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.2.3"
5
+ VERSION = "0.2.4"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -105,6 +105,10 @@ files:
105
105
  - MIT-LICENSE
106
106
  - README.md
107
107
  - Rakefile
108
+ - app/controllers/action_mcp/application_controller.rb
109
+ - app/controllers/action_mcp/messages_controller.rb
110
+ - app/controllers/action_mcp/sse_controller.rb
111
+ - config/routes.rb
108
112
  - exe/actionmcp_cli
109
113
  - lib/action_mcp.rb
110
114
  - lib/action_mcp/capability.rb