actionmcp 0.2.4 → 0.2.5

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: 0a1cd6d70ca62ff8ecf4d25c00e29d9592bc9d80ca46db50330a2f818a66ce1f
4
- data.tar.gz: 259e35b08f9627aa4165157b71436f4fdd4c0c9eaedc2ae41ddb6f0bb8c3fdde
3
+ metadata.gz: 60e4539fb902676529e53c949552d94deee59d23c4bade8b0f7cbd65f639fc23
4
+ data.tar.gz: 3bd3d5c01abbaf88f0e45e348f81ffe3aa3170c748e22a198c025af3e0fef146
5
5
  SHA512:
6
- metadata.gz: c213f50edbec5dc96af78b3090f562dae9fe479a405a44d573927ef65c1c042d6a43ef174297bca3bc92280a372d5a3a74c61aec5a8dfc3e1381550c56b27397
7
- data.tar.gz: 343bee907d44e4e7869372b5ffa250c6d628c8789ef51a4690b311c494d6b5b20199837dbbc5f39959e36b8a71d3ce2942b82685b4076897ddc393e59e73d8e3
6
+ metadata.gz: aa892c898a93934af62ab31f729602696b522516d041f2951e11b64bd02f375419c552b2e481b1743dc95632ba31092c5f8358e12001e2a26c47af1dfb57d334
7
+ data.tar.gz: dcb0d1ea6dedf6e845dc6279b58158007bc950687cdb8ceb8c1a1457f8280a8aa8c4a10f4f1cf1d8da54f7b4a26952dd62753ab2786654c4d0e15d7b0c8ab379
data/README.md CHANGED
@@ -86,6 +86,7 @@ ActionMCP includes Rails generators to help you quickly set up your MCP server c
86
86
  You can generate the base classes for your MCP Prompt and Tool using the following command:
87
87
 
88
88
  ```bash
89
+ bin/rails action_mcp:install:migrations # to copy the migrations
89
90
  bin/rails generate action_mcp:install
90
91
  ```
91
92
 
@@ -3,7 +3,7 @@ module ActionMCP
3
3
  # @route POST / (sse_in)
4
4
  def create
5
5
  begin
6
- handle_post_message(params, response)
6
+ handle_post_message(clean_params, response)
7
7
  rescue => e
8
8
  head :internal_server_error
9
9
  end
@@ -12,12 +12,8 @@ module ActionMCP
12
12
 
13
13
  private
14
14
 
15
- def transport
16
- @transport ||= Transport.new(session_key)
17
- end
18
-
19
15
  def transport_handler
20
- TransportHandler.new(transport)
16
+ TransportHandler.new(mcp_session)
21
17
  end
22
18
 
23
19
  def json_rpc_handler
@@ -29,23 +25,17 @@ module ActionMCP
29
25
 
30
26
  response.status = :accepted
31
27
  rescue StandardError => e
28
+ puts "Error: #{e.message}"
29
+ puts e.backtrace.join("\n")
32
30
  response.status = :bad_request
33
31
  end
34
32
 
35
- def session_id
36
- params[:session_id]
33
+ def mcp_session
34
+ Session.find(params[:session_id])
37
35
  end
38
36
 
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
37
+ def clean_params
38
+ params.slice(:id, :method, :jsonrpc, :params, :result, :error)
49
39
  end
50
40
  end
51
41
  end
@@ -1,6 +1,6 @@
1
1
  module ActionMCP
2
2
  class SSEController < ApplicationController
3
- HEARTBEAT_INTERVAL = 10
3
+ HEARTBEAT_INTERVAL = 30 # TODO: The frequency of pings SHOULD be configurable
4
4
  INITIALIZATION_TIMEOUT = 2
5
5
  include ActionController::Live
6
6
 
@@ -12,29 +12,22 @@ module ActionMCP
12
12
  response.headers["Cache-Control"] = "no-cache"
13
13
  response.headers["Connection"] = "keep-alive"
14
14
 
15
- listener = nil
16
- begin
17
- # Now start streaming - send endpoint
18
- send_endpoint_event(sse_in_url)
15
+ # Now start streaming - send endpoint
16
+ send_endpoint_event(sse_in_url)
19
17
 
18
+ begin
20
19
  # Start listener and process messages via the transport
21
- listener = SseListener.new(session_key)
20
+ listener = SSEListener.new(mcp_session)
22
21
  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
22
+ # Send with proper SSE formatting
23
+ sse = SSE.new(response.stream)
24
+ sse.write(message)
32
25
  end
33
26
 
34
27
  # Heartbeat loop
35
28
  until response.stream.closed?
36
29
  sleep HEARTBEAT_INTERVAL
37
- send_ping!
30
+ mcp_session.send_ping!
38
31
  end
39
32
  else
40
33
  Rails.logger.error "Listener failed to activate for session: #{session_id}"
@@ -45,7 +38,7 @@ module ActionMCP
45
38
  rescue => e
46
39
  Rails.logger.error "SSE: Unexpected error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
47
40
  ensure
48
- listener&.stop
41
+ listener.stop
49
42
  response.stream.close
50
43
  Rails.logger.debug "SSE: Connection closed for session: #{session_id}"
51
44
  end
@@ -60,32 +53,31 @@ module ActionMCP
60
53
  .write(endpoint)
61
54
  end
62
55
 
63
- def send_ping!
64
- SSE.new(response.stream,
65
- event: "ping")
66
- .write(Time.now.to_i)
67
- end
68
-
69
56
  def default_url_options
70
57
  { host: request.host, port: request.port }
71
58
  end
72
59
 
60
+ def mcp_session
61
+ @mcp_session ||= Session.create
62
+ end
63
+
73
64
  def session_id
74
- @session_id ||= SecureRandom.hex(6)
65
+ @session_id ||= mcp_session.id
75
66
  end
76
67
  end
77
68
 
78
- class SseListener
69
+ class SSEListener
79
70
  attr_reader :session_key, :adapter
71
+ delegate :session_key, :adapter, to: :@session
80
72
 
81
- def initialize(session_key)
82
- @session_key = session_key
83
- @adapter = ActionMCP::Server.server.pubsub
73
+ # @param session [ActionMCP::Session]
74
+ def initialize(session)
75
+ @session = session
84
76
  @stopped = false
85
77
  @subscription_active = false
86
78
  end
87
79
 
88
- # Start listening using ActionCable's PostgreSQL adapter
80
+ # Start listening using ActionCable's adapter
89
81
  def start(&callback)
90
82
  Rails.logger.debug "Starting listener for channel: #{session_key}"
91
83
 
@@ -97,17 +89,13 @@ module ActionMCP
97
89
 
98
90
  # Set up message callback with detailed debugging
99
91
  message_callback = ->(raw_message) {
100
- Rails.logger.debug "Received raw message via adapter: #{raw_message.inspect} (#{raw_message.class})"
101
-
102
92
  begin
103
93
  # 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}"
94
+ message = MultiJson.load(raw_message)
106
95
 
107
96
  # Send the message to the callback
108
97
  callback.call(message) if callback && !@stopped
109
98
  rescue => e
110
- Rails.logger.error "Error processing message: #{e.class} - #{e.message}"
111
99
  # Still try to send the raw message as a fallback
112
100
  callback.call(raw_message) if callback && !@stopped
113
101
  end
@@ -117,16 +105,9 @@ module ActionMCP
117
105
  adapter.subscribe(session_key, message_callback, success_callback)
118
106
 
119
107
  # 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
108
+ sleep 0.5
109
+
110
+ @subscription_active
130
111
  end
131
112
 
132
113
  def stop
@@ -136,8 +117,7 @@ module ActionMCP
136
117
  # Unsubscribe using the correct method signature
137
118
  begin
138
119
  # Create a dummy callback that matches the one we provided in start
139
- dummy_callback = ->(_) { }
140
- adapter.unsubscribe(session_key, dummy_callback)
120
+ @session.close!
141
121
  Rails.logger.debug "Unsubscribed from: #{session_key}"
142
122
  rescue => e
143
123
  Rails.logger.error "Error unsubscribing from #{session_key}: #{e.message}"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,91 @@
1
+ module ActionMCP
2
+ class Session::Message < ApplicationRecord
3
+ belongs_to :session,
4
+ class_name: "ActionMCP::Session",
5
+ inverse_of: :messages,
6
+ counter_cache: true
7
+
8
+ delegate :adapter,
9
+ :role,
10
+ :session_key,
11
+ to: :session
12
+
13
+ # Virtual attribute for data
14
+ attr_reader :data
15
+
16
+ after_create_commit :broadcast_message, if: :outgoing_message?
17
+
18
+ # @param payload [String, Hash]
19
+ def data=(payload)
20
+ @data = payload
21
+
22
+ # Store original version and attempt to determine type
23
+ if payload.is_a?(String)
24
+ self.message_text = payload
25
+
26
+ begin
27
+ parsed_json = MultiJson.load(payload)
28
+ self.message_json = parsed_json
29
+ process_json_content(parsed_json)
30
+ rescue MultiJson::ParseError
31
+ # Not valid JSON, just store as text
32
+ self.message_type = "text"
33
+ end
34
+ else
35
+ # Handle Hash or other JSON-serializable input
36
+ self.message_json = payload
37
+ self.message_text = MultiJson.dump(payload)
38
+ process_json_content(payload)
39
+ end
40
+ end
41
+
42
+ def data
43
+ message_json.presence || message_text
44
+ end
45
+
46
+ # Helper method to check if message is a particular type
47
+ def request?
48
+ message_type == "request"
49
+ end
50
+
51
+ def notification?
52
+ message_type == "notification"
53
+ end
54
+
55
+ def response?
56
+ message_type == "response"
57
+ end
58
+
59
+ private
60
+
61
+ def outgoing_message?
62
+ direction != role
63
+ end
64
+
65
+ def broadcast_message
66
+ adapter.broadcast(session_key, data.to_json)
67
+ end
68
+
69
+ def process_json_content(content)
70
+ # Determine message type based on JSON-RPC spec
71
+ if content.is_a?(Hash) && content["jsonrpc"] == "2.0"
72
+ if content.key?("id") && content.key?("method")
73
+ self.message_type = "request"
74
+ self.jsonrpc_id = content["id"]
75
+ elsif content.key?("method") && !content.key?("id")
76
+ self.message_type = "notification"
77
+ elsif content.key?("id") && content.key?("result")
78
+ self.message_type = "response"
79
+ self.jsonrpc_id = content["id"]
80
+ elsif content.key?("id") && content.key?("error")
81
+ self.message_type = "error"
82
+ self.jsonrpc_id = content["id"]
83
+ else
84
+ self.message_type = "invalid_jsonrpc"
85
+ end
86
+ else
87
+ self.message_type = "non_jsonrpc_json"
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,109 @@
1
+ module ActionMCP
2
+ class Session < ApplicationRecord
3
+ attribute :id, :string, default: -> { SecureRandom.hex(6) }
4
+ has_many :messages,
5
+ class_name: "ActionMCP::Session::Message",
6
+ foreign_key: "session_id",
7
+ dependent: :destroy,
8
+ inverse_of: :session
9
+
10
+ scope :pre_initialize, -> { where(status: "pre_initialize") }
11
+ scope :closed, -> { where(status: "closed") }
12
+ scope :without_messages, -> { includes(:messages).where(action_mcp_session_messages: { id: nil }) }
13
+
14
+ before_create :set_server_info
15
+ before_create :set_server_capabilities
16
+
17
+ validates :protocol_version, inclusion: { in: [ PROTOCOL_VERSION ] }, allow_nil: true
18
+
19
+ def close!
20
+ adapter.unsubscribe(session_key, _)
21
+ update!(status: "closed", ended_at: Time.zone.now)
22
+ end
23
+
24
+ def write(data)
25
+ if data.is_a?(JsonRpc::Request) || data.is_a?(JsonRpc::Response) || data.is_a?(JsonRpc::Notification)
26
+ data = data.to_json
27
+ end
28
+ if data.is_a?(Hash)
29
+ data = MultiJson.dump(data)
30
+ end
31
+
32
+ messages.create!(data: data, direction: writer_role)
33
+ end
34
+
35
+ def read(data)
36
+ puts "\e[33m[#{role}] #{data}\e[0m"
37
+ messages.create!(data: data, direction: role)
38
+ end
39
+
40
+ def session_key
41
+ "action_mcp:session:#{id}"
42
+ end
43
+
44
+ def adapter
45
+ @adapter ||= ActionMCP::Server.server.pubsub
46
+ end
47
+
48
+ def set_protocol_version(version)
49
+ update(protocol_version: version)
50
+ end
51
+
52
+ def store_client_info(info)
53
+ self.client_info = info
54
+ end
55
+
56
+ def store_client_capabilities(capabilities)
57
+ self.client_capabilities = capabilities
58
+ end
59
+
60
+ def server_capabilities_payload
61
+ {
62
+ protocolVersion: PROTOCOL_VERSION,
63
+ serverInfo: server_info,
64
+ capabilities: server_capabilities
65
+ }
66
+ end
67
+
68
+ def initialize!
69
+ # update the session initialized to true if client_capabilities are present
70
+ update!(initialized: true,
71
+ status: "initialized"
72
+ ) if client_capabilities.present?
73
+ end
74
+
75
+ def message_flow
76
+ messages.order(created_at: :asc).map do |message|
77
+ {
78
+ direction: message.direction,
79
+ data: message.data,
80
+ type: message.message_type
81
+ }
82
+ end
83
+ end
84
+
85
+ def send_ping!
86
+ write(JsonRpc::Request.new(id: Time.now.to_i, method: "ping"))
87
+ end
88
+
89
+ private
90
+
91
+ # if this session is from a server, the writer is the client
92
+ def writer_role
93
+ role == "server" ? "client" : "server"
94
+ end
95
+
96
+ # This will keep the version and name of the server when this session was created
97
+ def set_server_info
98
+ self.server_info = {
99
+ name: ActionMCP.configuration.name,
100
+ version: ActionMCP.configuration.version
101
+ }
102
+ end
103
+
104
+ # This can be overridden by the application in future versions
105
+ def set_server_capabilities
106
+ self.server_capabilities ||= ActionMCP.configuration.capabilities
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ module ActionMCP
2
+ def self.table_name_prefix
3
+ "action_mcp_"
4
+ end
5
+ end
@@ -45,7 +45,7 @@ module ActionMCP
45
45
  capabilities[:logging] = {} if @logging_enabled
46
46
  # capabilities[:resources] = { subscribe: @resources_subscribe,
47
47
  # listChanged: @list_changed }.compact
48
- { capabilities: capabilities }
48
+ capabilities
49
49
  end
50
50
  end
51
51
 
@@ -2,13 +2,17 @@
2
2
 
3
3
  module ActionMCP
4
4
  class JsonRpcHandler
5
+ delegate :initialize!, :initialized?, to: :transport
6
+ delegate :write, :read, to: :transport
5
7
  attr_reader :transport
6
8
 
9
+ # @param transport [ActionMCP::TransportHandler]
7
10
  def initialize(transport)
8
11
  @transport = transport
9
12
  end
10
13
 
11
14
  # Process a single line of input.
15
+ # @param line [String, Hash]
12
16
  def call(line)
13
17
  request = if line.is_a?(String)
14
18
  line.strip!
@@ -28,11 +32,16 @@ module ActionMCP
28
32
 
29
33
  private
30
34
 
35
+ # @param request [Hash]
31
36
  def process_request(request)
32
- unless request["jsonrpc"] == "2.0"
33
- puts "Invalid request: #{request}"
34
- return
35
- end
37
+ unless request["jsonrpc"] == "2.0"
38
+ puts "Invalid request: #{request}"
39
+ return
40
+ end
41
+ read(request)
42
+ return if request["error"]
43
+ return if request["result"] == {} # Probably a pong
44
+
36
45
  method = request["method"]
37
46
  id = request["id"]
38
47
  params = request["params"]
@@ -45,42 +54,80 @@ module ActionMCP
45
54
  transport.send_pong(id)
46
55
  when /^notifications\//
47
56
  puts "\e[31mProcessing notifications\e[0m"
48
- process_notifications(method, id, params)
57
+ process_notifications(method)
49
58
  when /^prompts\//
50
59
  process_prompts(method, id, params)
51
60
  when /^resources\//
52
61
  process_resources(method, id, params)
53
62
  when /^tools\//
54
63
  process_tools(method, id, params)
64
+ when "completion/complete"
65
+ process_completion_complete(id, params)
55
66
  else
56
- puts "\e[31mUnknown method: #{method}\e[0m"
57
- Rails.logger.warn("Unknown method: #{method}")
67
+ puts "\e[31mUnknown method: #{method} #{request}\e[0m"
58
68
  end
59
69
  end
60
70
 
61
- def process_notifications(method, _id, _params)
62
- case method
71
+ # @param rpc_method [String]
72
+ def process_notifications(rpc_method)
73
+ case rpc_method
63
74
  when "notifications/initialized"
64
75
  puts "\e[31mInitialized\e[0m"
65
- transport.initialized!
76
+ transport.initialize!
66
77
  else
67
- Rails.logger.warn("Unknown notifications method: #{method}")
78
+ Rails.logger.warn("Unknown notifications method: #{rpc_method}")
68
79
  end
69
80
  end
70
81
 
71
- def process_prompts(method, id, params)
72
- case method
82
+ # @param id [String]
83
+ # @param params [Hash]
84
+ # @example {
85
+ # "ref": {
86
+ # "type": "ref/prompt",
87
+ # "name": "code_review"
88
+ # },
89
+ # "argument": {
90
+ # "name": "language",
91
+ # "value": "py"
92
+ # }
93
+ # }
94
+ # @return [Hash]
95
+ # @example {
96
+ # "completion": {
97
+ # "values": ["python", "pytorch", "pyside"],
98
+ # "total": 10,
99
+ # "hasMore": true
100
+ # }
101
+ # }
102
+ def process_completion_complete(id, params)
103
+ case params["ref"]["type"]
104
+ when "ref/prompt"
105
+ # TODO: Implement completion
106
+ when "ref/resource"
107
+ # TODO: Implement completion
108
+ end
109
+ end
110
+
111
+ # @param rpc_method [String]
112
+ # @param id [String]
113
+ # @param params [Hash]
114
+ def process_prompts(rpc_method, id, params)
115
+ case rpc_method
73
116
  when "prompts/get"
74
- transport.send_prompts_get(id, params&.dig("name"), params&.dig("arguments"))
117
+ transport.send_prompts_get(id, params["name"], params["arguments"])
75
118
  when "prompts/list"
76
119
  transport.send_prompts_list(id)
77
120
  else
78
- Rails.logger.warn("Unknown prompts method: #{method}")
121
+ Rails.logger.warn("Unknown prompts method: #{rpc_method}")
79
122
  end
80
123
  end
81
124
 
82
- def process_resources(method, id, params)
83
- case method
125
+ # @param rpc_method [String]
126
+ # @param id [String]
127
+ # @param params [Hash]
128
+ # Not implemented
129
+ def process_resources(rpc_method, id, params)
130
+ case rpc_method
84
131
  when "resources/list"
85
132
  transport.send_resources_list(id)
86
133
  when "resources/templates/list"
@@ -88,18 +135,18 @@ module ActionMCP
88
135
  when "resources/read"
89
136
  transport.send_resource_read(id, params)
90
137
  else
91
- Rails.logger.warn("Unknown resources method: #{method}")
138
+ Rails.logger.warn("Unknown resources method: #{rpc_method}")
92
139
  end
93
140
  end
94
141
 
95
- def process_tools(method, id, params)
96
- case method
142
+ def process_tools(rpc_method, id, params)
143
+ case rpc_method
97
144
  when "tools/list"
98
145
  transport.send_tools_list(id)
99
146
  when "tools/call"
100
147
  transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
101
148
  else
102
- Rails.logger.warn("Unknown tools method: #{method}")
149
+ Rails.logger.warn("Unknown tools method: #{rpc_method}")
103
150
  end
104
151
  end
105
152
  end
@@ -26,8 +26,9 @@ module ActionMCP
26
26
  def self.default_prompt_name
27
27
  name.demodulize.underscore.sub(/_prompt$/, "")
28
28
  end
29
+
29
30
  class << self
30
- alias default_capability_name default_prompt_name
31
+ alias default_capability_name default_prompt_name
31
32
  end
32
33
 
33
34
  # ---------------------------------------------------
@@ -39,21 +40,26 @@ module ActionMCP
39
40
  # @param description [String] The description of the argument.
40
41
  # @param required [Boolean] Whether the argument is required.
41
42
  # @param default [Object] The default value of the argument.
43
+ # @param enum [Array<String>] The list of allowed values for the argument.
42
44
  # @return [void]
43
- def self.argument(arg_name, description: "", required: false, default: nil)
45
+ # Argument DSL
46
+ def self.argument(arg_name, description: "", required: false, default: nil, enum: nil)
44
47
  arg_def = {
45
48
  name: arg_name.to_s,
46
49
  description: description,
47
50
  required: required,
48
- default: default
51
+ default: default,
52
+ enum: enum
49
53
  }
50
54
  self._argument_definitions += [ arg_def ]
51
55
 
52
56
  # Register the attribute so it's recognized by ActiveModel
53
57
  attribute arg_name, :string, default: default
54
- return unless required
58
+ validates arg_name, presence: true if required
55
59
 
56
- validates arg_name, presence: true
60
+ if enum.present?
61
+ validates arg_name, inclusion: { in: enum }
62
+ end
57
63
  end
58
64
 
59
65
  # Returns the list of argument definitions.
@@ -5,16 +5,12 @@ module ActionMCP
5
5
  @protocol_version = params["protocolVersion"]
6
6
  @client_info = params["clientInfo"]
7
7
  @client_capabilities = params["capabilities"]
8
- capabilities = ActionMCP.configuration.capabilities
9
-
10
- payload = {
11
- protocolVersion: PROTOCOL_VERSION,
12
- serverInfo: {
13
- name: ActionMCP.configuration.name,
14
- version: ActionMCP.configuration.version
15
- }
16
- }.merge(capabilities)
17
- send_jsonrpc_response(request_id, result: payload)
8
+ session.store_client_info(@client_info)
9
+ session.store_client_capabilities(@client_capabilities)
10
+ session.set_protocol_version(@protocol_version)
11
+ session.save
12
+ # TODO , if the server don't support the protocol version, send a response with error
13
+ send_jsonrpc_response(request_id, result: session.server_capabilities_payload)
18
14
  end
19
15
  end
20
16
  end
@@ -3,17 +3,17 @@ module ActionMCP
3
3
  module Messaging
4
4
  def send_jsonrpc_request(method, params: nil, id: SecureRandom.uuid_v7)
5
5
  request = JsonRpc::Request.new(id: id, method: method, params: params)
6
- write_message(request.to_json)
6
+ write_message(request)
7
7
  end
8
8
 
9
9
  def send_jsonrpc_response(request_id, result: nil, error: nil)
10
10
  response = JsonRpc::Response.new(id: request_id, result: result, error: error)
11
- write_message(response.to_json)
11
+ write_message(response)
12
12
  end
13
13
 
14
14
  def send_jsonrpc_notification(method, params = nil)
15
15
  notification = JsonRpc::Notification.new(method: method, params: params)
16
- write_message(notification.to_json)
16
+ write_message(notification)
17
17
  end
18
18
  end
19
19
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module ActionMCP
4
4
  class TransportHandler
5
+ attr_reader :session
6
+ delegate :initialize!, :initialized?, to: :session
7
+ delegate :read, :write, to: :session
5
8
  include Logging
6
9
 
7
10
  include Transport::Capabilities
@@ -10,41 +13,20 @@ module ActionMCP
10
13
  include Transport::Messaging
11
14
 
12
15
  HEARTBEAT_INTERVAL = 15 # seconds
13
- attr_reader :initialized
14
-
15
- def initialize(output_io)
16
- @output = output_io
17
- @output.sync = true if @output.respond_to?(:sync=)
18
- @initialized = false
19
- @client_capabilities = {}
20
- @client_info = {}
21
- @protocol_version = ""
22
- end
23
16
 
24
- def send_ping
25
- send_jsonrpc_request("ping")
17
+ # @param [ActionMCP::Session] session
18
+ def initialize(session)
19
+ @session = session
26
20
  end
27
21
 
28
22
  def send_pong(request_id)
29
23
  send_jsonrpc_response(request_id, result: {})
30
24
  end
31
25
 
32
- def initialized?
33
- @initialized
34
- end
35
-
36
- def initialized!
37
- @initialized = true
38
- end
39
-
40
26
  private
41
27
 
42
28
  def write_message(data)
43
- Timeout.timeout(5) do
44
- @output.write("#{data}\n")
45
- end
46
- rescue Timeout::Error
47
- # ActionMCP.logger.error("Write operation timed out")
29
+ session.write(data)
48
30
  end
49
31
 
50
32
  def format_registry_items(registry)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.2.4"
5
+ VERSION = "0.2.5"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-11 00:00:00.000000000 Z
10
+ date: 2025-03-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -108,6 +108,10 @@ files:
108
108
  - app/controllers/action_mcp/application_controller.rb
109
109
  - app/controllers/action_mcp/messages_controller.rb
110
110
  - app/controllers/action_mcp/sse_controller.rb
111
+ - app/models/action_mcp.rb
112
+ - app/models/action_mcp/application_record.rb
113
+ - app/models/action_mcp/session.rb
114
+ - app/models/action_mcp/session/message.rb
111
115
  - config/routes.rb
112
116
  - exe/actionmcp_cli
113
117
  - lib/action_mcp.rb