actionmcp 0.50.1 → 0.50.2
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/unified_controller.rb +39 -58
- data/app/models/action_mcp/session/message.rb +25 -16
- data/app/models/action_mcp/session/sse_event.rb +1 -0
- data/app/models/action_mcp/session.rb +31 -27
- data/app/models/concerns/mcp_console_helpers.rb +3 -3
- data/app/models/concerns/mcp_message_inspect.rb +4 -4
- data/db/migrate/20250512154359_consolidated_migration.rb +28 -27
- data/exe/actionmcp_cli +1 -1
- data/lib/action_mcp/client.rb +4 -4
- data/lib/action_mcp/engine.rb +27 -0
- data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
- data/lib/action_mcp/json_rpc_handler_base.rb +1 -0
- data/lib/action_mcp/log_subscriber.rb +160 -0
- data/lib/action_mcp/resource_template.rb +1 -3
- data/lib/action_mcp/server/capabilities.rb +11 -8
- data/lib/action_mcp/server/configuration.rb +5 -2
- data/lib/action_mcp/server/json_rpc_handler.rb +155 -88
- data/lib/action_mcp/server/registry_management.rb +2 -0
- data/lib/action_mcp/server/simple_pub_sub.rb +7 -6
- data/lib/action_mcp/server/solid_cable_adapter.rb +12 -13
- data/lib/action_mcp/server/tools.rb +2 -2
- data/lib/action_mcp/server.rb +5 -4
- data/lib/action_mcp/tool.rb +1 -1
- data/lib/action_mcp/version.rb +1 -1
- metadata +20 -17
@@ -28,7 +28,7 @@ module ActionMCP
|
|
28
28
|
def log_process_action(payload)
|
29
29
|
messages = super
|
30
30
|
mcp_runtime = payload[:mcp_runtime]
|
31
|
-
messages <<
|
31
|
+
messages << format("MCP: %.1fms", mcp_runtime.to_f) if mcp_runtime
|
32
32
|
messages
|
33
33
|
end
|
34
34
|
end
|
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
# Thread-local storage for additional metrics
|
6
|
+
class << self
|
7
|
+
attr_accessor :custom_metrics, :subscribed_events, :formatters, :metric_groups
|
8
|
+
end
|
9
|
+
|
5
10
|
def self.reset_runtime
|
6
11
|
# Get the combined runtime from both tool and prompt operations
|
7
12
|
tool_rt = Thread.current[:mcp_tool_runtime] || 0
|
@@ -26,6 +31,161 @@ module ActionMCP
|
|
26
31
|
Thread.current[:mcp_prompt_runtime] += event.duration
|
27
32
|
end
|
28
33
|
|
34
|
+
# Add a custom metric to be included in logs
|
35
|
+
def self.add_metric(name, value)
|
36
|
+
self.custom_metrics ||= {}
|
37
|
+
self.custom_metrics[name] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
# Measure execution time of a block and add as metric
|
41
|
+
def self.measure_metric(name)
|
42
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
43
|
+
result = yield
|
44
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0
|
45
|
+
|
46
|
+
add_metric(name, duration)
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
# Reset all custom metrics
|
51
|
+
def self.reset_metrics
|
52
|
+
self.custom_metrics = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Subscribe to a Rails event to capture metrics
|
56
|
+
# @param pattern [String] Event name pattern (e.g., "sql.active_record")
|
57
|
+
# @param metric_name [Symbol] Name to use for the metric
|
58
|
+
# @param options [Hash] Options for capturing the metric
|
59
|
+
def self.subscribe_event(pattern, metric_name, options = {})
|
60
|
+
self.subscribed_events ||= {}
|
61
|
+
|
62
|
+
# Store subscription info
|
63
|
+
self.subscribed_events[pattern] = {
|
64
|
+
metric_name: metric_name,
|
65
|
+
options: options
|
66
|
+
}
|
67
|
+
|
68
|
+
# Create the actual subscription
|
69
|
+
ActiveSupport::Notifications.subscribe(pattern) do |*args|
|
70
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
71
|
+
|
72
|
+
# Extract value based on options
|
73
|
+
value = if options[:duration]
|
74
|
+
event.duration
|
75
|
+
elsif options[:extract_value].respond_to?(:call)
|
76
|
+
options[:extract_value].call(event)
|
77
|
+
else
|
78
|
+
1 # Default to count
|
79
|
+
end
|
80
|
+
|
81
|
+
# Accumulate or set the metric
|
82
|
+
if options[:accumulate]
|
83
|
+
self.custom_metrics ||= {}
|
84
|
+
self.custom_metrics[metric_name] ||= 0
|
85
|
+
self.custom_metrics[metric_name] += value
|
86
|
+
else
|
87
|
+
add_metric(metric_name, value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Format metrics for display in logs
|
93
|
+
def self.format_metrics
|
94
|
+
return nil if custom_metrics.nil? || custom_metrics.empty?
|
95
|
+
|
96
|
+
# If grouping is enabled, organize metrics by groups
|
97
|
+
if metric_groups.present?
|
98
|
+
grouped_metrics = {}
|
99
|
+
|
100
|
+
# Initialize groups with empty arrays
|
101
|
+
metric_groups.each_key do |group_name|
|
102
|
+
grouped_metrics[group_name] = []
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add "other" group for ungrouped metrics
|
106
|
+
grouped_metrics[:other] = []
|
107
|
+
|
108
|
+
# Assign metrics to their groups
|
109
|
+
custom_metrics.each do |key, value|
|
110
|
+
group = nil
|
111
|
+
|
112
|
+
# Find which group this metric belongs to
|
113
|
+
metric_groups.each do |group_name, metrics|
|
114
|
+
if metrics.include?(key)
|
115
|
+
group = group_name
|
116
|
+
break
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Format the metric
|
121
|
+
formatter = formatters&.dig(key)
|
122
|
+
formatted_value = if formatter.respond_to?(:call)
|
123
|
+
formatter.call(value)
|
124
|
+
elsif value.is_a?(Float)
|
125
|
+
format("%.1fms", value)
|
126
|
+
else
|
127
|
+
value.to_s
|
128
|
+
end
|
129
|
+
|
130
|
+
formatted_metric = "#{key}: #{formatted_value}"
|
131
|
+
|
132
|
+
# Add to appropriate group (or "other")
|
133
|
+
if group
|
134
|
+
grouped_metrics[group] << formatted_metric
|
135
|
+
else
|
136
|
+
grouped_metrics[:other] << formatted_metric
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Join metrics within groups, then join groups
|
141
|
+
grouped_metrics.map do |_group, metrics|
|
142
|
+
next if metrics.empty?
|
143
|
+
|
144
|
+
metrics.join(" | ")
|
145
|
+
end.compact.join(" | ")
|
146
|
+
else
|
147
|
+
# No grouping, just format all metrics
|
148
|
+
custom_metrics.map do |key, value|
|
149
|
+
formatter = formatters&.dig(key)
|
150
|
+
formatted_value = if formatter.respond_to?(:call)
|
151
|
+
formatter.call(value)
|
152
|
+
elsif value.is_a?(Float)
|
153
|
+
format("%.1fms", value)
|
154
|
+
else
|
155
|
+
value.to_s
|
156
|
+
end
|
157
|
+
"#{key}: #{formatted_value}"
|
158
|
+
end.join(" | ")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Register a custom formatter for a specific metric
|
163
|
+
# @param metric_name [Symbol] The name of the metric
|
164
|
+
# @param block [Proc] The formatter block that takes the value and returns a string
|
165
|
+
def self.register_formatter(metric_name, &block)
|
166
|
+
self.formatters ||= {}
|
167
|
+
self.formatters[metric_name] = block
|
168
|
+
end
|
169
|
+
|
170
|
+
# Define a group of related metrics
|
171
|
+
# @param group_name [Symbol] The name of the metric group
|
172
|
+
# @param metrics [Array<Symbol>] The metrics that belong to this group
|
173
|
+
def self.define_metric_group(group_name, metrics)
|
174
|
+
self.metric_groups ||= {}
|
175
|
+
self.metric_groups[group_name] = metrics
|
176
|
+
end
|
177
|
+
|
178
|
+
# Enhance process_action to include our custom metrics
|
179
|
+
def process_action(event)
|
180
|
+
return unless logger.info?
|
181
|
+
|
182
|
+
return unless self.class.custom_metrics.present?
|
183
|
+
|
184
|
+
metrics_msg = self.class.format_metrics
|
185
|
+
event.payload[:message] = "#{event.payload[:message]} | #{metrics_msg}" if metrics_msg
|
186
|
+
self.class.reset_metrics
|
187
|
+
end
|
188
|
+
|
29
189
|
attach_to :action_mcp
|
30
190
|
end
|
31
191
|
end
|
@@ -13,7 +13,7 @@ module ActionMCP
|
|
13
13
|
|
14
14
|
# Track all registered templates
|
15
15
|
@registered_templates = []
|
16
|
-
attr_reader :execution_context
|
16
|
+
attr_reader :execution_context, :description, :uri_template, :mime_type
|
17
17
|
|
18
18
|
class << self
|
19
19
|
attr_reader :registered_templates, :description, :uri_template,
|
@@ -227,8 +227,6 @@ module ActionMCP
|
|
227
227
|
end
|
228
228
|
end
|
229
229
|
|
230
|
-
attr_reader :description, :uri_template, :mime_type
|
231
|
-
|
232
230
|
def call
|
233
231
|
run_callbacks :resolve do
|
234
232
|
resolve
|
@@ -16,7 +16,8 @@ module ActionMCP
|
|
16
16
|
|
17
17
|
unless client_protocol_version.is_a?(String) && client_protocol_version.present?
|
18
18
|
send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
|
19
|
-
return { type: :error, id: request_id,
|
19
|
+
return { type: :error, id: request_id,
|
20
|
+
payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'protocolVersion'" } } }
|
20
21
|
end
|
21
22
|
# Check if the protocol version is supported
|
22
23
|
unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
|
@@ -25,20 +26,21 @@ module ActionMCP
|
|
25
26
|
requested: client_protocol_version
|
26
27
|
}
|
27
28
|
send_jsonrpc_error(request_id, :invalid_params, "Unsupported protocol version", error_data)
|
28
|
-
return { type: :error, id: request_id,
|
29
|
+
return { type: :error, id: request_id,
|
30
|
+
payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Unsupported protocol version", data: error_data } } }
|
29
31
|
end
|
30
32
|
|
31
33
|
unless client_info.is_a?(Hash)
|
32
34
|
send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'clientInfo'")
|
33
|
-
return { type: :error, id: request_id,
|
35
|
+
return { type: :error, id: request_id,
|
36
|
+
payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'clientInfo'" } } }
|
34
37
|
end
|
35
38
|
unless client_capabilities.is_a?(Hash)
|
36
39
|
send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'capabilities'")
|
37
|
-
return { type: :error, id: request_id,
|
40
|
+
return { type: :error, id: request_id,
|
41
|
+
payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'capabilities'" } } }
|
38
42
|
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
44
|
# Store client information
|
43
45
|
session.store_client_info(client_info)
|
44
46
|
session.store_client_capabilities(client_capabilities)
|
@@ -47,12 +49,13 @@ module ActionMCP
|
|
47
49
|
# Initialize the session
|
48
50
|
unless session.initialize!
|
49
51
|
send_jsonrpc_error(request_id, :internal_error, "Failed to initialize session")
|
50
|
-
return { type: :error, id: request_id,
|
52
|
+
return { type: :error, id: request_id,
|
53
|
+
payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_603, message: "Failed to initialize session" } } }
|
51
54
|
end
|
52
55
|
|
53
56
|
# Send the successful response with the protocol version the client requested
|
54
57
|
capabilities_payload = session.server_capabilities_payload
|
55
|
-
capabilities_payload[:protocolVersion] = client_protocol_version
|
58
|
+
capabilities_payload[:protocolVersion] = client_protocol_version # Use the client's requested version
|
56
59
|
|
57
60
|
send_jsonrpc_response(request_id, result: capabilities_payload)
|
58
61
|
{ type: :responses, id: request_id, payload: { jsonrpc: "2.0", id: request_id, result: capabilities_payload } }
|
@@ -39,8 +39,10 @@ module ActionMCP
|
|
39
39
|
|
40
40
|
yaml = ERB.new(File.read(@config_path)).result
|
41
41
|
YAML.safe_load(yaml, aliases: true) || {}
|
42
|
-
rescue => e
|
43
|
-
|
42
|
+
rescue StandardError => e
|
43
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
44
|
+
Rails.logger.error("Error loading ActionMCP config: #{e.message}")
|
45
|
+
end
|
44
46
|
{}
|
45
47
|
end
|
46
48
|
|
@@ -52,6 +54,7 @@ module ActionMCP
|
|
52
54
|
while path != "/"
|
53
55
|
config_path = File.join(path, "config", "mcp.yml")
|
54
56
|
return config_path if File.exist?(config_path)
|
57
|
+
|
55
58
|
path = File.dirname(path)
|
56
59
|
end
|
57
60
|
|
@@ -4,37 +4,30 @@ module ActionMCP
|
|
4
4
|
module Server
|
5
5
|
class JsonRpcHandler < JsonRpcHandlerBase
|
6
6
|
# Handle server-specific methods
|
7
|
-
# @param
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
rescue MultiJson::ParseError => e
|
18
|
-
Rails.logger.error("Failed to parse JSON: #{e.message}")
|
19
|
-
return
|
20
|
-
end
|
21
|
-
else
|
22
|
-
line
|
7
|
+
# @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
|
8
|
+
def call(request)
|
9
|
+
read(request.to_h)
|
10
|
+
case request
|
11
|
+
when JSON_RPC::Request
|
12
|
+
handle_request(request)
|
13
|
+
when JSON_RPC::Notification
|
14
|
+
handle_notification(request)
|
15
|
+
when JSON_RPC::Response
|
16
|
+
handle_response(request)
|
23
17
|
end
|
24
|
-
|
25
|
-
# Store the request ID for error responses
|
26
|
-
@current_request_id = request["id"] if request.is_a?(Hash)
|
27
|
-
|
28
|
-
process_request(request)
|
29
18
|
end
|
30
19
|
|
31
|
-
|
32
|
-
|
33
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def handle_request(request)
|
23
|
+
@current_request_id = id = request.id
|
24
|
+
rpc_method = request.method
|
25
|
+
params = request.params
|
34
26
|
|
35
27
|
case rpc_method
|
36
28
|
when "initialize"
|
37
|
-
transport.send_capabilities(id, params)
|
29
|
+
message = transport.send_capabilities(id, params)
|
30
|
+
extract_message_payload(message, id)
|
38
31
|
when %r{^prompts/}
|
39
32
|
process_prompts(rpc_method, id, params)
|
40
33
|
when %r{^resources/}
|
@@ -44,104 +37,178 @@ module ActionMCP
|
|
44
37
|
when "completion/complete"
|
45
38
|
process_completion_complete(id, params)
|
46
39
|
else
|
47
|
-
|
40
|
+
error_response(id, :method_not_found, "Method not found: #{rpc_method}")
|
48
41
|
end
|
49
|
-
rescue StandardError => e
|
50
|
-
Rails.logger.error("Error handling method #{rpc_method}: #{e.message}")
|
51
|
-
transport.send_jsonrpc_error(id, :internal_error, "Internal error: #{e.message}")
|
52
42
|
end
|
53
43
|
|
44
|
+
def handle_notification(notification)
|
45
|
+
@current_request_id = nil
|
54
46
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# "name": "language",
|
66
|
-
# "value": "py"
|
67
|
-
# }
|
68
|
-
# }
|
69
|
-
# @return [Hash]
|
70
|
-
# @example {
|
71
|
-
# "completion": {
|
72
|
-
# "values": ["python", "pytorch", "pyside"],
|
73
|
-
# "total": 10,
|
74
|
-
# "hasMore": true
|
75
|
-
# }
|
76
|
-
# }
|
77
|
-
def process_completion_complete(id, params)
|
78
|
-
# TODO: Not Implemented, but to remove the error message in the inspector
|
79
|
-
transport.send_jsonrpc_response(id, result: { completion: { values: [], total: 0, hasMore: false } })
|
80
|
-
case params["ref"]["type"]
|
81
|
-
when "ref/prompt"
|
82
|
-
# TODO: Implement completion
|
83
|
-
when "ref/resource"
|
84
|
-
# TODO: Implement completion
|
47
|
+
begin
|
48
|
+
method_name = notification.method.to_s
|
49
|
+
params = notification.params || {}
|
50
|
+
|
51
|
+
process_notifications(method_name, params)
|
52
|
+
{ type: :notifications_only }
|
53
|
+
rescue StandardError => e
|
54
|
+
Rails.logger.error("Error handling notification #{notification.method}: #{e.message}")
|
55
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
56
|
+
{ type: :notifications_only }
|
85
57
|
end
|
86
58
|
end
|
87
59
|
|
88
|
-
|
89
|
-
|
90
|
-
|
60
|
+
def handle_response(response)
|
61
|
+
Rails.logger.debug("Received response: #{response.inspect}")
|
62
|
+
{
|
63
|
+
type: :responses,
|
64
|
+
request_id: response.id,
|
65
|
+
payload: {
|
66
|
+
jsonrpc: "2.0",
|
67
|
+
id: response.id,
|
68
|
+
result: response.result
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
91
73
|
def process_prompts(rpc_method, id, params)
|
74
|
+
params ||= {}
|
75
|
+
|
92
76
|
case rpc_method
|
93
|
-
when "prompts/get"
|
94
|
-
|
95
|
-
|
96
|
-
transport.
|
77
|
+
when "prompts/get"
|
78
|
+
name = params["name"] || params[:name]
|
79
|
+
arguments = params["arguments"] || params[:arguments] || {}
|
80
|
+
message = transport.send_prompts_get(id, name, arguments)
|
81
|
+
extract_message_payload(message, id)
|
82
|
+
when "prompts/list"
|
83
|
+
message = transport.send_prompts_list(id)
|
84
|
+
extract_message_payload(message, id)
|
97
85
|
else
|
98
86
|
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
87
|
+
error_response(id, :method_not_found, "Unknown prompts method: #{rpc_method}")
|
99
88
|
end
|
100
89
|
end
|
101
90
|
|
102
|
-
# @param rpc_method [String]
|
103
|
-
# @param id [String]
|
104
|
-
# @param params [Hash]
|
105
91
|
def process_tools(rpc_method, id, params)
|
92
|
+
params ||= {}
|
93
|
+
|
106
94
|
case rpc_method
|
107
|
-
when "tools/list"
|
108
|
-
transport.send_tools_list(id, params)
|
109
|
-
|
110
|
-
|
95
|
+
when "tools/list"
|
96
|
+
message = transport.send_tools_list(id, params)
|
97
|
+
extract_message_payload(message, id)
|
98
|
+
when "tools/call"
|
99
|
+
name = params["name"] || params[:name]
|
100
|
+
arguments = params["arguments"] || params[:arguments] || {}
|
101
|
+
|
102
|
+
return error_response(id, :invalid_params, "Tool name is required") if name.nil?
|
103
|
+
|
104
|
+
message = transport.send_tools_call(id, name, arguments)
|
105
|
+
extract_message_payload(message, id)
|
111
106
|
else
|
112
107
|
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
108
|
+
error_response(id, :method_not_found, "Unknown tools method: #{rpc_method}")
|
113
109
|
end
|
114
110
|
end
|
115
111
|
|
116
|
-
# @param rpc_method [String]
|
117
|
-
# @param id [String]
|
118
|
-
# @param params [Hash]
|
119
112
|
def process_resources(rpc_method, id, params)
|
113
|
+
params ||= {}
|
114
|
+
|
120
115
|
case rpc_method
|
121
|
-
when "resources/list"
|
122
|
-
transport.send_resources_list(id)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
when "resources/
|
128
|
-
|
129
|
-
|
130
|
-
transport.
|
116
|
+
when "resources/list"
|
117
|
+
message = transport.send_resources_list(id)
|
118
|
+
extract_message_payload(message, id)
|
119
|
+
when "resources/templates/list"
|
120
|
+
message = transport.send_resource_templates_list(id)
|
121
|
+
extract_message_payload(message, id)
|
122
|
+
when "resources/read"
|
123
|
+
return error_response(id, :invalid_params, "Resource URI is required") if params.nil? || params.empty?
|
124
|
+
|
125
|
+
message = transport.send_resource_read(id, params)
|
126
|
+
extract_message_payload(message, id)
|
127
|
+
when "resources/subscribe"
|
128
|
+
uri = params["uri"] || params[:uri]
|
129
|
+
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
130
|
+
|
131
|
+
message = transport.send_resource_subscribe(id, uri)
|
132
|
+
extract_message_payload(message, id)
|
133
|
+
when "resources/unsubscribe"
|
134
|
+
uri = params["uri"] || params[:uri]
|
135
|
+
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
136
|
+
|
137
|
+
message = transport.send_resource_unsubscribe(id, uri)
|
138
|
+
extract_message_payload(message, id)
|
131
139
|
else
|
132
140
|
Rails.logger.warn("Unknown resources method: #{rpc_method}")
|
141
|
+
error_response(id, :method_not_found, "Unknown resources method: #{rpc_method}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def process_completion_complete(id, params)
|
146
|
+
params ||= {}
|
147
|
+
|
148
|
+
result = transport.send_jsonrpc_response(id, result: {
|
149
|
+
completion: { values: [], total: 0, hasMore: false }
|
150
|
+
})
|
151
|
+
|
152
|
+
if result.is_a?(ActionMCP::Session::Message)
|
153
|
+
extract_message_payload(result, id)
|
154
|
+
else
|
155
|
+
wrap_transport_result(result, id)
|
133
156
|
end
|
134
157
|
end
|
135
158
|
|
136
159
|
def process_notifications(rpc_method, params)
|
137
160
|
case rpc_method
|
138
|
-
when "notifications/initialized"
|
139
|
-
|
161
|
+
when "notifications/initialized"
|
162
|
+
Rails.logger.info "Client notified initialization complete"
|
140
163
|
transport.initialize!
|
141
164
|
else
|
142
165
|
super
|
143
166
|
end
|
144
167
|
end
|
168
|
+
|
169
|
+
def extract_message_payload(message, id)
|
170
|
+
if message.is_a?(ActionMCP::Session::Message)
|
171
|
+
{
|
172
|
+
type: :responses,
|
173
|
+
request_id: id,
|
174
|
+
payload: message.message_json
|
175
|
+
}
|
176
|
+
else
|
177
|
+
message
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def wrap_transport_result(transport_result, id)
|
182
|
+
if transport_result.is_a?(Hash) && transport_result[:type]
|
183
|
+
transport_result
|
184
|
+
else
|
185
|
+
{
|
186
|
+
type: :responses,
|
187
|
+
request_id: id,
|
188
|
+
payload: transport_result
|
189
|
+
}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def error_response(id, code, message)
|
194
|
+
error_code = case code
|
195
|
+
when :method_not_found then -32_601
|
196
|
+
when :invalid_params then -32_602
|
197
|
+
when :internal_error then -32_603
|
198
|
+
else -32_000
|
199
|
+
end
|
200
|
+
|
201
|
+
{
|
202
|
+
type: :error,
|
203
|
+
request_id: id,
|
204
|
+
payload: {
|
205
|
+
jsonrpc: "2.0",
|
206
|
+
id: id,
|
207
|
+
error: { code: error_code, message: message }
|
208
|
+
},
|
209
|
+
status: :bad_request
|
210
|
+
}
|
211
|
+
end
|
145
212
|
end
|
146
213
|
end
|
147
214
|
end
|
@@ -13,7 +13,7 @@ module ActionMCP
|
|
13
13
|
DEFAULT_MIN_THREADS = 5
|
14
14
|
DEFAULT_MAX_THREADS = 10
|
15
15
|
DEFAULT_MAX_QUEUE = 100
|
16
|
-
DEFAULT_THREAD_TIMEOUT = 60
|
16
|
+
DEFAULT_THREAD_TIMEOUT = 60 # seconds
|
17
17
|
|
18
18
|
def initialize(options = {})
|
19
19
|
@subscriptions = Concurrent::Map.new
|
@@ -58,6 +58,7 @@ module ActionMCP
|
|
58
58
|
def subscribed_to?(channel)
|
59
59
|
channel_subs = @channels[channel]
|
60
60
|
return false if channel_subs.nil?
|
61
|
+
|
61
62
|
!channel_subs.empty?
|
62
63
|
end
|
63
64
|
|
@@ -91,11 +92,9 @@ module ActionMCP
|
|
91
92
|
next unless subscription && subscription[:message_callback]
|
92
93
|
|
93
94
|
@thread_pool.post do
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
98
|
-
end
|
95
|
+
subscription[:message_callback].call(message)
|
96
|
+
rescue StandardError => e
|
97
|
+
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
99
98
|
end
|
100
99
|
end
|
101
100
|
end
|
@@ -106,6 +105,7 @@ module ActionMCP
|
|
106
105
|
def has_subscribers?(channel)
|
107
106
|
subscribers = @channels[channel]
|
108
107
|
return false unless subscribers
|
108
|
+
|
109
109
|
!subscribers.empty?
|
110
110
|
end
|
111
111
|
|
@@ -138,6 +138,7 @@ module ActionMCP
|
|
138
138
|
|
139
139
|
def log_error(message)
|
140
140
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
141
|
+
|
141
142
|
Rails.logger.error("SimplePubSub: #{message}")
|
142
143
|
end
|
143
144
|
end
|