actionmcp 0.50.1 → 0.50.3
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 → application_controller.rb} +118 -144
- 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/config/routes.rb +3 -3
- 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 +16 -17
- data/app/controllers/action_mcp/mcp_controller.rb +0 -79
@@ -167,9 +167,7 @@ module ActionMCP
|
|
167
167
|
)
|
168
168
|
|
169
169
|
# Maintain cache limit by removing oldest events if needed
|
170
|
-
if sse_events.count > max_events
|
171
|
-
sse_events.order(event_id: :asc).limit(sse_events.count - max_events).destroy_all
|
172
|
-
end
|
170
|
+
sse_events.order(event_id: :asc).limit(sse_events.count - max_events).destroy_all if sse_events.count > max_events
|
173
171
|
|
174
172
|
event
|
175
173
|
end
|
@@ -240,10 +238,10 @@ module ActionMCP
|
|
240
238
|
tool_name = normalize_name(tool_class_or_name, :tool)
|
241
239
|
self.tool_registry ||= []
|
242
240
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
241
|
+
return unless self.tool_registry.delete(tool_name)
|
242
|
+
|
243
|
+
save!
|
244
|
+
send_tools_list_changed_notification
|
247
245
|
end
|
248
246
|
|
249
247
|
def register_prompt(prompt_class_or_name)
|
@@ -263,10 +261,10 @@ module ActionMCP
|
|
263
261
|
prompt_name = normalize_name(prompt_class_or_name, :prompt)
|
264
262
|
self.prompt_registry ||= []
|
265
263
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
264
|
+
return unless self.prompt_registry.delete(prompt_name)
|
265
|
+
|
266
|
+
save!
|
267
|
+
send_prompts_list_changed_notification
|
270
268
|
end
|
271
269
|
|
272
270
|
def register_resource_template(template_class_or_name)
|
@@ -286,28 +284,34 @@ module ActionMCP
|
|
286
284
|
template_name = normalize_name(template_class_or_name, :resource_template)
|
287
285
|
self.resource_registry ||= []
|
288
286
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
287
|
+
return unless self.resource_registry.delete(template_name)
|
288
|
+
|
289
|
+
save!
|
290
|
+
send_resources_list_changed_notification
|
293
291
|
end
|
294
292
|
|
295
293
|
# Get registered items for this session
|
296
294
|
def registered_tools
|
297
295
|
(self.tool_registry || []).filter_map do |tool_name|
|
298
|
-
ActionMCP::ToolsRegistry.find(tool_name)
|
296
|
+
ActionMCP::ToolsRegistry.find(tool_name)
|
297
|
+
rescue StandardError
|
298
|
+
nil
|
299
299
|
end
|
300
300
|
end
|
301
301
|
|
302
302
|
def registered_prompts
|
303
303
|
(self.prompt_registry || []).filter_map do |prompt_name|
|
304
|
-
ActionMCP::PromptsRegistry.find(prompt_name)
|
304
|
+
ActionMCP::PromptsRegistry.find(prompt_name)
|
305
|
+
rescue StandardError
|
306
|
+
nil
|
305
307
|
end
|
306
308
|
end
|
307
309
|
|
308
310
|
def registered_resource_templates
|
309
311
|
(self.resource_registry || []).filter_map do |template_name|
|
310
|
-
ActionMCP::ResourceTemplatesRegistry.find(template_name)
|
312
|
+
ActionMCP::ResourceTemplatesRegistry.find(template_name)
|
313
|
+
rescue StandardError
|
314
|
+
nil
|
311
315
|
end
|
312
316
|
end
|
313
317
|
|
@@ -379,21 +383,21 @@ module ActionMCP
|
|
379
383
|
|
380
384
|
def send_tools_list_changed_notification
|
381
385
|
# Only send if server capabilities allow it
|
382
|
-
|
383
|
-
|
384
|
-
|
386
|
+
return unless server_capabilities.dig("tools", "listChanged")
|
387
|
+
|
388
|
+
write(JSON_RPC::Notification.new(method: "notifications/tools/list_changed"))
|
385
389
|
end
|
386
390
|
|
387
391
|
def send_prompts_list_changed_notification
|
388
|
-
|
389
|
-
|
390
|
-
|
392
|
+
return unless server_capabilities.dig("prompts", "listChanged")
|
393
|
+
|
394
|
+
write(JSON_RPC::Notification.new(method: "notifications/prompts/list_changed"))
|
391
395
|
end
|
392
396
|
|
393
397
|
def send_resources_list_changed_notification
|
394
|
-
|
395
|
-
|
396
|
-
|
398
|
+
return unless server_capabilities.dig("resources", "listChanged")
|
399
|
+
|
400
|
+
write(JSON_RPC::Notification.new(method: "notifications/resources/list_changed"))
|
397
401
|
end
|
398
402
|
end
|
399
403
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# app/models/concerns/mcp_console_helpers.rb
|
2
4
|
module MCPConsoleHelpers
|
3
5
|
extend ActiveSupport::Concern
|
@@ -12,9 +14,7 @@ module MCPConsoleHelpers
|
|
12
14
|
|
13
15
|
messages.each do |msg|
|
14
16
|
puts msg.inspect
|
15
|
-
if msg.data&.dig("method")
|
16
|
-
puts " └─ #{msg.data['method']}"
|
17
|
-
end
|
17
|
+
puts " └─ #{msg.data['method']}" if msg.data&.dig("method")
|
18
18
|
puts
|
19
19
|
end
|
20
20
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# app/models/concerns/mcp_message_inspect.rb
|
2
4
|
module MCPMessageInspect
|
3
5
|
extend ActiveSupport::Concern
|
4
6
|
|
5
7
|
def inspect(show_data: false)
|
6
8
|
if show_data
|
7
|
-
super()
|
9
|
+
super() # Rails default inspect
|
8
10
|
else
|
9
11
|
build_summary_inspect
|
10
12
|
end
|
@@ -54,9 +56,7 @@ module MCPMessageInspect
|
|
54
56
|
|
55
57
|
def console?
|
56
58
|
# Check if we're in a Rails console environment
|
57
|
-
defined?(Rails::Console)
|
58
|
-
defined?(::Rails.application) && Rails.application.console? ||
|
59
|
-
(defined?(IRB) && IRB.CurrentContext.kind_of?(IRB::ExtendCommandBundle))
|
59
|
+
defined?(Rails::Console)
|
60
60
|
end
|
61
61
|
|
62
62
|
def colorize(text, color)
|
data/config/routes.rb
CHANGED
@@ -4,7 +4,7 @@ ActionMCP::Engine.routes.draw do
|
|
4
4
|
get "/up", to: "/rails/health#show", as: :action_mcp_health_check
|
5
5
|
|
6
6
|
# MCP 2025-03-26 Spec routes
|
7
|
-
get "/", to: "
|
8
|
-
post "/", to: "
|
9
|
-
delete "/", to: "
|
7
|
+
get "/", to: "application#show", as: :mcp_get
|
8
|
+
post "/", to: "application#create", as: :mcp_post
|
9
|
+
delete "/", to: "application#destroy", as: :mcp_delete
|
10
10
|
end
|
@@ -29,10 +29,10 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
29
29
|
unless table_exists?(:action_mcp_session_messages)
|
30
30
|
create_table :action_mcp_session_messages do |t|
|
31
31
|
t.references :session, null: false,
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
foreign_key: { to_table: :action_mcp_sessions,
|
33
|
+
on_delete: :cascade,
|
34
|
+
on_update: :cascade,
|
35
|
+
name: 'fk_action_mcp_session_messages_session_id' }, type: :string
|
36
36
|
t.string :direction, null: false, comment: 'The message recipient', default: 'client'
|
37
37
|
t.string :message_type, null: false, comment: 'The type of the message'
|
38
38
|
t.string :jsonrpc_id
|
@@ -48,9 +48,9 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
48
48
|
unless table_exists?(:action_mcp_session_subscriptions)
|
49
49
|
create_table :action_mcp_session_subscriptions do |t|
|
50
50
|
t.references :session,
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
null: false,
|
52
|
+
foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
|
53
|
+
type: :string
|
54
54
|
t.string :uri, null: false
|
55
55
|
t.datetime :last_notification_at
|
56
56
|
t.timestamps
|
@@ -61,9 +61,9 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
61
61
|
unless table_exists?(:action_mcp_session_resources)
|
62
62
|
create_table :action_mcp_session_resources do |t|
|
63
63
|
t.references :session,
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
null: false,
|
65
|
+
foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
|
66
|
+
type: :string
|
67
67
|
t.string :uri, null: false
|
68
68
|
t.string :name
|
69
69
|
t.text :description
|
@@ -84,7 +84,7 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
84
84
|
t.timestamps
|
85
85
|
|
86
86
|
# Index for efficiently retrieving events after a given ID for a specific session
|
87
|
-
t.index [
|
87
|
+
t.index %i[session_id event_id], unique: true
|
88
88
|
t.index :created_at # For cleanup of old events
|
89
89
|
end
|
90
90
|
end
|
@@ -111,27 +111,28 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
111
111
|
end
|
112
112
|
|
113
113
|
# For action_mcp_session_messages
|
114
|
-
|
115
|
-
unless column_exists?(:action_mcp_session_messages, :is_ping)
|
116
|
-
add_column :action_mcp_session_messages, :is_ping, :boolean, default: false, null: false, comment: 'Whether the message is a ping'
|
117
|
-
end
|
114
|
+
return unless table_exists?(:action_mcp_session_messages)
|
118
115
|
|
119
|
-
|
120
|
-
|
121
|
-
|
116
|
+
unless column_exists?(:action_mcp_session_messages, :is_ping)
|
117
|
+
add_column :action_mcp_session_messages, :is_ping, :boolean, default: false, null: false,
|
118
|
+
comment: 'Whether the message is a ping'
|
119
|
+
end
|
122
120
|
|
123
|
-
|
124
|
-
|
125
|
-
|
121
|
+
unless column_exists?(:action_mcp_session_messages, :request_acknowledged)
|
122
|
+
add_column :action_mcp_session_messages, :request_acknowledged, :boolean, default: false, null: false
|
123
|
+
end
|
126
124
|
|
127
|
-
|
128
|
-
|
129
|
-
|
125
|
+
unless column_exists?(:action_mcp_session_messages, :request_cancelled)
|
126
|
+
add_column :action_mcp_session_messages, :request_cancelled, :boolean, null: false, default: false
|
127
|
+
end
|
130
128
|
|
131
|
-
|
132
|
-
|
133
|
-
end
|
129
|
+
if column_exists?(:action_mcp_session_messages, :message_text)
|
130
|
+
remove_column :action_mcp_session_messages, :message_text
|
134
131
|
end
|
132
|
+
|
133
|
+
return unless column_exists?(:action_mcp_session_messages, :direction)
|
134
|
+
|
135
|
+
change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
|
135
136
|
end
|
136
137
|
|
137
138
|
private
|
data/exe/actionmcp_cli
CHANGED
@@ -56,7 +56,7 @@ if endpoint.nil?
|
|
56
56
|
end
|
57
57
|
|
58
58
|
unless endpoint =~ %r{\Ahttps?://}
|
59
|
-
puts
|
59
|
+
puts 'Error: Only HTTP(S) endpoints are supported. STDIO/command endpoints are not allowed.'
|
60
60
|
exit 1
|
61
61
|
end
|
62
62
|
|
data/lib/action_mcp/client.rb
CHANGED
@@ -13,12 +13,12 @@ module ActionMCP
|
|
13
13
|
# client = ActionMCP.create_client("http://127.0.0.1:3001/action_mcp")
|
14
14
|
# client.connect
|
15
15
|
def self.create_client(endpoint, logger: Logger.new($stdout), **options)
|
16
|
-
|
17
|
-
logger.info("Creating SSE client for endpoint: #{endpoint}")
|
18
|
-
Client::SSEClient.new(endpoint, logger: logger, **options)
|
19
|
-
else
|
16
|
+
unless endpoint =~ %r{\Ahttps?://}
|
20
17
|
raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
|
21
18
|
end
|
19
|
+
|
20
|
+
logger.info("Creating SSE client for endpoint: #{endpoint}")
|
21
|
+
Client::SSEClient.new(endpoint, logger: logger, **options)
|
22
22
|
end
|
23
23
|
|
24
24
|
module Client
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -47,5 +47,32 @@ module ActionMCP
|
|
47
47
|
initializer "action_mcp.logger" do
|
48
48
|
ActionMCP.logger = ::Rails.logger
|
49
49
|
end
|
50
|
+
|
51
|
+
# Add metrics instrumentation
|
52
|
+
initializer "action_mcp.metrics" do
|
53
|
+
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload|
|
54
|
+
if payload[:controller].to_s.start_with?("ActionMCP::")
|
55
|
+
# Process action through our log subscriber
|
56
|
+
ActionMCP::LogSubscriber.new.process_action(
|
57
|
+
ActiveSupport::Notifications::Event.new(name, start, finish, id, payload)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set up default event metrics
|
63
|
+
# SQL queries
|
64
|
+
ActionMCP::LogSubscriber.subscribe_event "sql.active_record", :db_queries, accumulate: true
|
65
|
+
|
66
|
+
# Query runtime
|
67
|
+
ActionMCP::LogSubscriber.subscribe_event "sql.active_record", :sql_runtime,
|
68
|
+
duration: true, accumulate: true
|
69
|
+
|
70
|
+
# View rendering
|
71
|
+
ActionMCP::LogSubscriber.subscribe_event "render_template.action_view", :view_runtime,
|
72
|
+
duration: true, accumulate: true
|
73
|
+
|
74
|
+
# Cache operations
|
75
|
+
ActionMCP::LogSubscriber.subscribe_event "cache_*.*", :cache_operations, accumulate: true
|
76
|
+
end
|
50
77
|
end
|
51
78
|
end
|
@@ -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
|
|