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.
- checksums.yaml +4 -4
- data/app/controllers/action_mcp/mcp_controller.rb +69 -1
- data/app/controllers/action_mcp/sse_controller.rb +13 -78
- data/app/controllers/action_mcp/unified_controller.rb +304 -0
- data/app/models/action_mcp/session.rb +13 -2
- data/config/routes.rb +7 -0
- data/db/migrate/20250327124131_add_sse_event_counter_to_action_mcp_sessions.rb +7 -0
- data/exe/actionmcp_cli +5 -5
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/messaging.rb +5 -5
- data/lib/action_mcp/configuration.rb +18 -16
- data/lib/action_mcp/server/capabilities.rb +45 -10
- data/lib/action_mcp/server/messaging.rb +4 -4
- data/lib/action_mcp/sse_listener.rb +83 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -4
- data/lib/tasks/action_mcp_tasks.rake +7 -7
- metadata +20 -11
- data/lib/action_mcp/json_rpc/json_rpc_error.rb +0 -91
- data/lib/action_mcp/json_rpc/notification.rb +0 -27
- data/lib/action_mcp/json_rpc/request.rb +0 -46
- data/lib/action_mcp/json_rpc/response.rb +0 -80
- data/lib/action_mcp/json_rpc.rb +0 -7
@@ -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
|
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
|
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
|
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
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/action_mcp/version.rb
CHANGED
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 |
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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.
|
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-
|
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
|
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
|