actionmcp 0.29.0 → 0.30.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.
@@ -21,7 +21,12 @@ module ActionMCP
21
21
  :resources_subscribe,
22
22
  :logging_level,
23
23
  :active_profile,
24
- :profiles
24
+ :profiles,
25
+ # --- New Transport Options ---
26
+ :allow_client_session_termination,
27
+ :mcp_endpoint_path,
28
+ :sse_heartbeat_interval,
29
+ :post_response_preference # :json or :sse
25
30
 
26
31
  def initialize
27
32
  @logging_enabled = true
@@ -30,6 +35,11 @@ module ActionMCP
30
35
  @resources_subscribe = false
31
36
  @active_profile = :primary
32
37
  @profiles = default_profiles
38
+
39
+ @allow_client_session_termination = true
40
+ @mcp_endpoint_path = "/mcp"
41
+ @sse_heartbeat_interval = 30
42
+ @post_response_preference = :json
33
43
  end
34
44
 
35
45
  def name
@@ -58,7 +68,7 @@ module ActionMCP
58
68
 
59
69
  # Merge with defaults so user config overrides gem defaults
60
70
  @profiles = app_config
61
- rescue => e
71
+ rescue StandardError
62
72
  # If the config file doesn't exist in the Rails app, just use the defaults
63
73
  Rails.logger.debug "No MCP config found in Rails app, using defaults from gem"
64
74
  end
@@ -114,21 +124,13 @@ module ActionMCP
114
124
  capabilities = {}
115
125
 
116
126
  # Only include capabilities if the corresponding filtered registry is non-empty
117
- if filtered_tools.any?
118
- capabilities[:tools] = { listChanged: @list_changed }
119
- end
127
+ capabilities[:tools] = { listChanged: @list_changed } if filtered_tools.any?
120
128
 
121
- if filtered_prompts.any?
122
- capabilities[:prompts] = { listChanged: @list_changed }
123
- end
129
+ capabilities[:prompts] = { listChanged: @list_changed } if filtered_prompts.any?
124
130
 
125
- if @logging_enabled
126
- capabilities[:logging] = {}
127
- end
131
+ capabilities[:logging] = {} if @logging_enabled
128
132
 
129
- if filtered_resources.any?
130
- capabilities[:resources] = { subscribe: @resources_subscribe }
131
- end
133
+ capabilities[:resources] = { subscribe: @resources_subscribe } if filtered_resources.any?
132
134
 
133
135
  capabilities
134
136
  end
@@ -178,7 +180,7 @@ module ActionMCP
178
180
 
179
181
  items = @profiles[active_profile][type]
180
182
  # Return true ONLY if items contains "all"
181
- items && items.include?("all")
183
+ items&.include?("all")
182
184
  end
183
185
 
184
186
  def has_rails_version
@@ -213,7 +215,7 @@ module ActionMCP
213
215
  thread_profiles.value = profile_name
214
216
 
215
217
  # Apply the profile options when switching profiles
216
- configuration.apply_profile_options if configuration
218
+ configuration&.apply_profile_options
217
219
 
218
220
  yield if block_given?
219
221
  ensure
@@ -3,17 +3,52 @@
3
3
  module ActionMCP
4
4
  module Server
5
5
  module Capabilities
6
+ # Handles the 'initialize' request. Validates parameters, checks protocol version,
7
+ # stores client info, initializes the session, and returns the server capabilities payload
8
+ # or an error payload.
9
+ # @param request_id [String, Integer] The JSON-RPC request ID.
10
+ # @param params [Hash] The JSON-RPC parameters.
11
+ # @return [Hash] A hash representing the JSON-RPC response (success or error).
6
12
  def send_capabilities(request_id, params = {})
7
- # TODO: fix this if client send incorrect params
8
- # TODO refuse connection if protocol version is not supported
9
- @protocol_version = params["protocolVersion"]
10
- @client_info = params["clientInfo"]
11
- @client_capabilities = params["capabilities"]
12
- session.store_client_info(@client_info)
13
- session.store_client_capabilities(@client_capabilities)
14
- session.set_protocol_version(@protocol_version)
15
- session.initialize!
16
- # TODO: , if the server don't support the protocol version, send a response with error
13
+ # 1. Validate Parameters
14
+ client_protocol_version = params["protocolVersion"]
15
+ client_info = params["clientInfo"]
16
+ client_capabilities = params["capabilities"]
17
+
18
+ unless client_protocol_version.is_a?(String) && client_protocol_version.present?
19
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
20
+ end
21
+ # Basic check, could be more specific based on spec requirements
22
+ unless client_info.is_a?(Hash)
23
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'clientInfo'")
24
+ end
25
+ unless client_capabilities.is_a?(Hash)
26
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'capabilities'")
27
+ end
28
+
29
+ # 2. Check Protocol Version
30
+ server_protocol_version = ActionMCP::PROTOCOL_VERSION
31
+ unless client_protocol_version == server_protocol_version
32
+ error_data = {
33
+ supported: [ server_protocol_version ],
34
+ requested: client_protocol_version
35
+ }
36
+ # Using -32602 Invalid Params code as per spec example for version mismatch
37
+ return send_jsonrpc_error(request_id, :invalid_params, "Unsupported protocol version", error_data)
38
+ end
39
+
40
+ # 3. Store Info and Initialize Session
41
+ session.store_client_info(client_info)
42
+ session.store_client_capabilities(client_capabilities)
43
+ session.set_protocol_version(client_protocol_version) # Store the agreed-upon version
44
+
45
+ # Attempt to initialize (this saves the session if new)
46
+ unless session.initialize!
47
+ # Handle potential initialization failure (e.g., validation error on save)
48
+ return send_jsonrpc_error(request_id, :internal_error, "Failed to initialize session")
49
+ end
50
+
51
+ # 4. Return Success Response Payload
17
52
  send_jsonrpc_response(request_id, result: session.server_capabilities_payload)
18
53
  end
19
54
  end
@@ -16,7 +16,7 @@ module ActionMCP
16
16
  end
17
17
 
18
18
  def send_jsonrpc_error(request_id, symbol, message, data = nil)
19
- error = JsonRpc::JsonRpcError.new(symbol, message: message, data: data)
19
+ error = JSON_RPC::JsonRpcError.new(symbol, message: message, data: data)
20
20
  send_jsonrpc_response(request_id, error: error)
21
21
  end
22
22
 
@@ -26,19 +26,19 @@ module ActionMCP
26
26
  def send_message(type, **args)
27
27
  message = case type
28
28
  when :request
29
- JsonRpc::Request.new(
29
+ JSON_RPC::Request.new(
30
30
  id: args[:id],
31
31
  method: args[:method],
32
32
  params: args[:params]
33
33
  )
34
34
  when :response
35
- JsonRpc::Response.new(
35
+ JSON_RPC::Response.new(
36
36
  id: args[:id],
37
37
  result: args[:result],
38
38
  error: args[:error]
39
39
  )
40
40
  when :notification
41
- JsonRpc::Notification.new(
41
+ JSON_RPC::Notification.new(
42
42
  method: args[:method],
43
43
  params: args[:params]
44
44
  )
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/promise"
5
+
6
+ module ActionMCP
7
+ # Listener class to subscribe to session messages via Action Cable adapter.
8
+ # Used by controllers handling Server-Sent Events streams.
9
+ class SSEListener
10
+ attr_reader :session_key, :adapter
11
+
12
+ delegate :session_key, :adapter, to: :@session
13
+
14
+ # @param session [ActionMCP::Session]
15
+ def initialize(session)
16
+ @session = session
17
+ @stopped = Concurrent::AtomicBoolean.new(false)
18
+ @subscription_active = Concurrent::AtomicBoolean.new(false)
19
+ end
20
+
21
+ # Start listening using ActionCable's adapter
22
+ # @yield [Hash] Yields parsed message received from the pub/sub channel
23
+ # @return [Boolean] True if subscription was successful within timeout, false otherwise.
24
+ def start(&callback)
25
+ Rails.logger.debug "SSEListener: Starting for channel: #{session_key}"
26
+
27
+ success_callback = lambda {
28
+ Rails.logger.info "SSEListener: Successfully subscribed to channel: #{session_key}"
29
+ @subscription_active.make_true
30
+ }
31
+
32
+ # Set up message callback
33
+ message_callback = lambda { |raw_message|
34
+ return if @stopped.true?
35
+
36
+ begin
37
+ # Try to parse the message if it's JSON
38
+ message = MultiJson.load(raw_message)
39
+ # Send the message to the callback
40
+ # TODO: Add SSE event ID here if implementing resumability
41
+ 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
46
+ end
47
+ }
48
+
49
+ # Subscribe using the ActionCable adapter
50
+ adapter.subscribe(session_key, message_callback, success_callback)
51
+
52
+ # Use a future with timeout to check subscription status
53
+ subscription_future = Concurrent::Promises.future do
54
+ sleep 0.1 while !@subscription_active.true? && !@stopped.true?
55
+ @subscription_active.true?
56
+ end
57
+
58
+ # Wait up to 5 seconds for subscription to be established (increased timeout)
59
+ begin
60
+ subscription_result = subscription_future.value(5)
61
+ subscription_result || @subscription_active.true?
62
+ rescue Concurrent::TimeoutError
63
+ Rails.logger.warn "SSEListener: Timed out waiting for subscription activation for #{session_key}"
64
+ false
65
+ end
66
+ 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
+ end
83
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.29.0"
5
+ VERSION = "0.30.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -6,6 +6,7 @@ require "active_support/rails"
6
6
  require "multi_json"
7
7
  require "concurrent"
8
8
  require "active_record/railtie"
9
+ require "jsonrpc-rails"
9
10
  require "action_controller/railtie"
10
11
  require "action_cable/engine"
11
12
  require "action_mcp/configuration"
@@ -26,6 +27,7 @@ Zeitwerk::Loader.for_gem.tap do |loader|
26
27
  loader.inflector.inflect("action_mcp" => "ActionMCP")
27
28
  loader.inflector.inflect("sse_client" => "SSEClient")
28
29
  loader.inflector.inflect("sse_server" => "SSEServer")
30
+ loader.inflector.inflect("sse_listener" => "SSEListener")
29
31
  end.setup
30
32
 
31
33
  module ActionMCP
@@ -86,7 +88,3 @@ module ActionMCP
86
88
  ActiveModel::Type.register(:string_array, StringArray)
87
89
  ActiveModel::Type.register(:integer_array, IntegerArray)
88
90
  end
89
-
90
- ActiveSupport.on_load(:action_mcp, run_once: true) do
91
- self.logger = ::Rails.logger
92
- end
@@ -51,7 +51,7 @@ namespace :action_mcp do
51
51
 
52
52
  # bin/rails action_mcp:show_profile[profile_name]
53
53
  desc "Show configuration for a specific profile"
54
- task :show_profile, [ :profile_name ] => :environment do |t, args|
54
+ task :show_profile, [ :profile_name ] => :environment do |_t, args|
55
55
  # Ensure Rails eager loads all classes
56
56
  Rails.application.eager_load!
57
57
 
@@ -68,19 +68,19 @@ namespace :action_mcp do
68
68
  ActionMCP.with_profile(profile_name) do
69
69
  profile_config = profiles[profile_name]
70
70
 
71
- puts "\e[35mPROFILE: #{profile_name.to_s.upcase}\e[0m" # Purple
71
+ puts "\e[35mPROFILE: #{profile_name.to_s.upcase}\e[0m" # Purple
72
72
  puts "\e[35m#{'-' * (profile_name.to_s.length + 9)}\e[0m"
73
73
 
74
74
  # Show options
75
75
  if profile_config[:options]
76
- puts "\n\e[36mOptions:\e[0m" # Cyan
76
+ puts "\n\e[36mOptions:\e[0m" # Cyan
77
77
  profile_config[:options].each do |key, value|
78
78
  puts " #{key}: #{value}"
79
79
  end
80
80
  end
81
81
 
82
82
  # Show Tools
83
- puts "\n\e[34mIncluded Tools:\e[0m" # Blue
83
+ puts "\n\e[34mIncluded Tools:\e[0m" # Blue
84
84
  if ActionMCP.configuration.filtered_tools.any?
85
85
  ActionMCP.configuration.filtered_tools.each do |tool|
86
86
  puts " \e[34m#{tool.name}:\e[0m #{tool.description}"
@@ -90,7 +90,7 @@ namespace :action_mcp do
90
90
  end
91
91
 
92
92
  # Show Prompts
93
- puts "\n\e[32mIncluded Prompts:\e[0m" # Green
93
+ puts "\n\e[32mIncluded Prompts:\e[0m" # Green
94
94
  if ActionMCP.configuration.filtered_prompts.any?
95
95
  ActionMCP.configuration.filtered_prompts.each do |prompt|
96
96
  puts " \e[32m#{prompt.name}:\e[0m #{prompt.description}"
@@ -100,7 +100,7 @@ namespace :action_mcp do
100
100
  end
101
101
 
102
102
  # Show Resources
103
- puts "\n\e[33mIncluded Resources:\e[0m" # Yellow
103
+ puts "\n\e[33mIncluded Resources:\e[0m" # Yellow
104
104
  if ActionMCP.configuration.filtered_resources.any?
105
105
  ActionMCP.configuration.filtered_resources.each do |resource|
106
106
  puts " \e[33m#{resource.name}:\e[0m #{resource.description}"
@@ -110,7 +110,7 @@ namespace :action_mcp do
110
110
  end
111
111
 
112
112
  # Show Capabilities
113
- puts "\n\e[36mActive Capabilities:\e[0m" # Cyan
113
+ puts "\n\e[36mActive Capabilities:\e[0m" # Cyan
114
114
  capabilities = ActionMCP.configuration.capabilities
115
115
  capabilities.each do |cap_name, cap_config|
116
116
  puts " #{cap_name}: #{cap_config.inspect}"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-03-25 00:00:00.000000000 Z
10
+ date: 2025-03-27 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actioncable
@@ -80,6 +79,20 @@ dependencies:
80
79
  - - "~>"
81
80
  - !ruby/object:Gem::Version
82
81
  version: '2.6'
82
+ - !ruby/object:Gem::Dependency
83
+ name: jsonrpc-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
83
96
  description: It offers base classes and helpers for creating MCP applications, making
84
97
  it easier to integrate your Ruby/Rails application with the MCP standard
85
98
  email:
@@ -95,6 +108,7 @@ files:
95
108
  - app/controllers/action_mcp/mcp_controller.rb
96
109
  - app/controllers/action_mcp/messages_controller.rb
97
110
  - app/controllers/action_mcp/sse_controller.rb
111
+ - app/controllers/action_mcp/unified_controller.rb
98
112
  - app/models/action_mcp.rb
99
113
  - app/models/action_mcp/application_record.rb
100
114
  - app/models/action_mcp/session.rb
@@ -107,6 +121,7 @@ files:
107
121
  - db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb
108
122
  - db/migrate/20250316005649_create_action_mcp_session_resources.rb
109
123
  - db/migrate/20250324203409_remove_session_message_text.rb
124
+ - db/migrate/20250327124131_add_sse_event_counter_to_action_mcp_sessions.rb
110
125
  - exe/actionmcp_cli
111
126
  - lib/action_mcp.rb
112
127
  - lib/action_mcp/base_response.rb
@@ -142,11 +157,6 @@ files:
142
157
  - lib/action_mcp/instrumentation/instrumentation.rb
143
158
  - lib/action_mcp/instrumentation/resource_instrumentation.rb
144
159
  - lib/action_mcp/integer_array.rb
145
- - lib/action_mcp/json_rpc.rb
146
- - lib/action_mcp/json_rpc/json_rpc_error.rb
147
- - lib/action_mcp/json_rpc/notification.rb
148
- - lib/action_mcp/json_rpc/request.rb
149
- - lib/action_mcp/json_rpc/response.rb
150
160
  - lib/action_mcp/json_rpc_handler_base.rb
151
161
  - lib/action_mcp/log_subscriber.rb
152
162
  - lib/action_mcp/logging.rb
@@ -171,6 +181,7 @@ files:
171
181
  - lib/action_mcp/server/sampling_request.rb
172
182
  - lib/action_mcp/server/tools.rb
173
183
  - lib/action_mcp/server/transport_handler.rb
184
+ - lib/action_mcp/sse_listener.rb
174
185
  - lib/action_mcp/string_array.rb
175
186
  - lib/action_mcp/test_helper.rb
176
187
  - lib/action_mcp/tool.rb
@@ -200,7 +211,6 @@ metadata:
200
211
  source_code_uri: https://github.com/seuros/action_mcp
201
212
  changelog_uri: https://github.com/seuros/action_mcp/blob/master/CHANGELOG.md
202
213
  rubygems_mfa_required: 'true'
203
- post_install_message:
204
214
  rdoc_options: []
205
215
  require_paths:
206
216
  - lib
@@ -215,8 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
225
  - !ruby/object:Gem::Version
216
226
  version: '0'
217
227
  requirements: []
218
- rubygems_version: 3.5.22
219
- signing_key:
228
+ rubygems_version: 3.6.5
220
229
  specification_version: 4
221
230
  summary: Provides essential tooling for building Model Context Protocol (MCP) capable
222
231
  servers
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module JsonRpc
5
- # Custom exception class for JSON-RPC errors, based on the JSON-RPC 2.0 specification.
6
- class JsonRpcError < StandardError
7
- # Define the standard JSON-RPC 2.0 error codes
8
- ERROR_CODES = {
9
- parse_error: {
10
- code: -32_700,
11
- message: "Parse error"
12
- },
13
- invalid_request: {
14
- code: -32_600,
15
- message: "Invalid request"
16
- },
17
- method_not_found: {
18
- code: -32_601,
19
- message: "Method not found"
20
- },
21
- invalid_params: {
22
- code: -32_602,
23
- message: "Invalid params"
24
- },
25
- internal_error: {
26
- code: -32_603,
27
- message: "Internal error"
28
- },
29
- server_error: {
30
- code: -32_000,
31
- message: "Server error"
32
- }
33
- }.freeze
34
-
35
- # @return [Integer] The error code.
36
- # @return [Object] The error data.
37
- attr_reader :code, :data
38
-
39
- # Retrieve error details by symbol.
40
- #
41
- # @param symbol [Symbol] The error symbol.
42
- # @raise [ArgumentError] if the error code is unknown.
43
- # @return [Hash] The error details.
44
- def self.[](symbol)
45
- ERROR_CODES[symbol] or raise ArgumentError, "Unknown error code: #{symbol}"
46
- end
47
-
48
- # Build an error hash, allowing custom message or data to override defaults.
49
- #
50
- # @param symbol [Symbol] The error symbol.
51
- # @param message [String, nil] Optional custom message.
52
- # @param data [Object, nil] Optional custom data.
53
- # @return [Hash] The error hash.
54
- def self.build(symbol, message: nil, data: nil)
55
- error = self[symbol].dup
56
- error[:message] = message if message
57
- error[:data] = data if data
58
- error
59
- end
60
-
61
- # Initialize the error using a symbol key, with optional custom message and data.
62
- #
63
- # @param symbol [Symbol] The error symbol.
64
- # @param message [String, nil] Optional custom message.
65
- # @param data [Object, nil] Optional custom data.
66
- def initialize(symbol, message: nil, data: nil)
67
- error_details = self.class.build(symbol, message: message, data: data)
68
- @code = error_details[:code]
69
- @data = error_details[:data]
70
- super(error_details[:message])
71
- end
72
-
73
- # Returns a hash formatted for a JSON-RPC error response.
74
- #
75
- # @return [Hash] The error hash.
76
- def to_h
77
- hash = { code: code, message: message }
78
- hash[:data] = data if data
79
- hash
80
- end
81
-
82
- # Converts the error hash to a JSON string.
83
- #
84
- # @param _args [Array] Arguments passed to MultiJson.dump.
85
- # @return [String] The JSON string.
86
- def to_json(*_args)
87
- MultiJson.dump(to_h, *args)
88
- end
89
- end
90
- end
91
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module JsonRpc
5
- # Represents a JSON-RPC notification.
6
- Notification = Data.define(:method, :params) do
7
- # Initializes a new Notification.
8
- #
9
- # @param method [String] The method name.
10
- # @param params [Hash, nil] The parameters (optional).
11
- def initialize(method:, params: nil)
12
- super
13
- end
14
-
15
- # Returns a hash representation of the notification.
16
- #
17
- # @return [Hash] The hash representation.
18
- def to_h
19
- {
20
- jsonrpc: "2.0",
21
- method: method,
22
- params: params
23
- }.compact
24
- end
25
- end
26
- end
27
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module JsonRpc
5
- # Represents a JSON-RPC request.
6
- Request = Data.define(:id, :method, :params) do
7
- # Initializes a new Request.
8
- #
9
- # @param id [String, Numeric] The request identifier.
10
- # @param method [String] The method name.
11
- # @param params [Hash, nil] The parameters (optional).
12
- # @raise [JsonRpcError] if the ID is invalid.
13
- def initialize(id:, method:, params: nil)
14
- validate_id(id)
15
- super
16
- end
17
-
18
- # Returns a hash representation of the request.
19
- #
20
- # @return [Hash] The hash representation.
21
- def to_h
22
- hash = {
23
- jsonrpc: "2.0",
24
- id: id,
25
- method: method
26
- }
27
- hash[:params] = params if params
28
- hash
29
- end
30
-
31
- private
32
-
33
- # Validates the ID.
34
- #
35
- # @param id [Object] The ID to validate.
36
- # @raise [JsonRpcError] if the ID is invalid.
37
- def validate_id(id)
38
- unless id.is_a?(String) || id.is_a?(Numeric)
39
- raise JsonRpcError.new(:invalid_params,
40
- message: "ID must be a string or number")
41
- end
42
- raise JsonRpcError.new(:invalid_params, message: "ID must not be null") if id.nil?
43
- end
44
- end
45
- end
46
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module JsonRpc
5
- # Represents a JSON-RPC response.
6
- Response = Data.define(:id, :result, :error) do
7
- # Initializes a new Response.
8
- #
9
- # @param id [String, Numeric] The request identifier.
10
- # @param result [Object, nil] The result data (optional).
11
- # @param error [Object, nil] The error data (optional).
12
- # @raise [ArgumentError] if neither result nor error is provided, or if both are provided.
13
- def initialize(id:, result: nil, error: nil)
14
- validate_presence_of_result_or_error!(result, error)
15
- validate_absence_of_both_result_and_error!(result, error)
16
- result, error = transform_value_to_hash!(result, error)
17
-
18
- super(id: id, result: result, error: error)
19
- end
20
-
21
- # Returns a hash representation of the response.
22
- #
23
- # @return [Hash] The hash representation.
24
- def to_h
25
- {
26
- jsonrpc: "2.0",
27
- id: id,
28
- result: result,
29
- error: error
30
- }.compact
31
- end
32
-
33
- def is_error?
34
- error.present?
35
- end
36
-
37
- private
38
-
39
- # Validates that either result or error is present.
40
- #
41
- # @param result [Object, nil] The result data.
42
- # @param error [Object, nil] The error data.
43
- # @raise [ArgumentError] if neither result nor error is provided.
44
- def validate_presence_of_result_or_error!(result, error)
45
- raise ArgumentError, "Either result or error must be provided." if result.nil? && error.nil?
46
- end
47
-
48
- # Validates that both result and error are not present simultaneously.
49
- #
50
- # @param result [Object, nil] The result data.
51
- # @param error [Object, nil] The error data.
52
- # @raise [ArgumentError] if both result and error are provided.
53
- def validate_absence_of_both_result_and_error!(result, error)
54
- raise ArgumentError, "Both result and error cannot be provided simultaneously." if result && error
55
- end
56
-
57
- def transform_value_to_hash!(result, error)
58
- result = if result.is_a?(String)
59
- begin
60
- MultiJson.load(result)
61
- rescue StandardError
62
- result
63
- end
64
- else
65
- result
66
- end
67
- error = if error.is_a?(String)
68
- begin
69
- MultiJson.load(error)
70
- rescue StandardError
71
- error
72
- end
73
- else
74
- error
75
- end
76
- [ result, error ]
77
- end
78
- end
79
- end
80
- end