actionmcp 0.14.0 → 0.17.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/README.md +174 -144
- data/Rakefile +1 -1
- data/app/controllers/action_mcp/{application_controller.rb → mcp_controller.rb} +3 -1
- data/app/controllers/action_mcp/messages_controller.rb +7 -5
- data/app/controllers/action_mcp/sse_controller.rb +19 -13
- data/app/models/action_mcp/session/message.rb +95 -90
- data/app/models/action_mcp/session/resource.rb +10 -6
- data/app/models/action_mcp/session/subscription.rb +9 -5
- data/app/models/action_mcp/session.rb +22 -13
- data/app/models/action_mcp.rb +2 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20250308122801_create_action_mcp_sessions.rb +12 -10
- data/db/migrate/20250314230152_add_is_ping_to_session_message.rb +2 -0
- data/db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb +3 -1
- data/db/migrate/20250316005649_create_action_mcp_session_resources.rb +4 -2
- data/exe/actionmcp_cli +57 -55
- data/lib/action_mcp/base_json_rpc_handler.rb +97 -0
- data/lib/action_mcp/callbacks.rb +122 -0
- data/lib/action_mcp/capability.rb +6 -3
- data/lib/action_mcp/client.rb +20 -26
- data/lib/action_mcp/client_json_rpc_handler.rb +69 -0
- data/lib/action_mcp/configuration.rb +8 -8
- data/lib/action_mcp/content/resource.rb +1 -1
- data/lib/action_mcp/gem_version.rb +2 -0
- data/lib/action_mcp/instrumentation/controller_runtime.rb +37 -0
- data/lib/action_mcp/instrumentation/instrumentation.rb +26 -0
- data/lib/action_mcp/instrumentation/resource_instrumentation.rb +40 -0
- data/lib/action_mcp/json_rpc/response.rb +18 -2
- data/lib/action_mcp/json_rpc_handler.rb +93 -21
- data/lib/action_mcp/log_subscriber.rb +29 -0
- data/lib/action_mcp/logging.rb +1 -3
- data/lib/action_mcp/prompt.rb +15 -6
- data/lib/action_mcp/prompt_response.rb +1 -1
- data/lib/action_mcp/prompts_registry.rb +1 -0
- data/lib/action_mcp/registry_base.rb +1 -0
- data/lib/action_mcp/resource_callbacks.rb +156 -0
- data/lib/action_mcp/resource_template.rb +25 -19
- data/lib/action_mcp/resource_templates_registry.rb +19 -25
- data/lib/action_mcp/sampling_request.rb +113 -0
- data/lib/action_mcp/server.rb +4 -1
- data/lib/action_mcp/server_json_rpc_handler.rb +90 -0
- data/lib/action_mcp/test_helper.rb +6 -2
- data/lib/action_mcp/tool.rb +12 -3
- data/lib/action_mcp/tool_response.rb +3 -2
- data/lib/action_mcp/transport/capabilities.rb +5 -1
- data/lib/action_mcp/transport/messaging.rb +2 -0
- data/lib/action_mcp/transport/prompts.rb +2 -0
- data/lib/action_mcp/transport/resources.rb +23 -6
- data/lib/action_mcp/transport/roots.rb +11 -0
- data/lib/action_mcp/transport/sampling.rb +14 -0
- data/lib/action_mcp/transport/sse_client.rb +11 -15
- data/lib/action_mcp/transport/stdio_client.rb +12 -14
- data/lib/action_mcp/transport/tools.rb +2 -0
- data/lib/action_mcp/transport/transport_base.rb +16 -15
- data/lib/action_mcp/transport.rb +2 -0
- data/lib/action_mcp/transport_handler.rb +3 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +8 -2
- data/lib/generators/action_mcp/install/install_generator.rb +4 -1
- data/lib/generators/action_mcp/install/templates/application_mcp_res_template.rb +2 -0
- data/lib/generators/action_mcp/resource_template/resource_template_generator.rb +2 -0
- data/lib/generators/action_mcp/resource_template/templates/resource_template.rb.erb +1 -1
- data/lib/tasks/action_mcp_tasks.rake +11 -6
- metadata +26 -14
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Base handler for common functionality
|
5
|
+
class BaseJsonRpcHandler
|
6
|
+
delegate :initialize!, :initialized?, to: :transport
|
7
|
+
delegate :write, :read, to: :transport
|
8
|
+
attr_reader :transport
|
9
|
+
|
10
|
+
# @param transport [ActionMCP::TransportHandler]
|
11
|
+
def initialize(transport)
|
12
|
+
@transport = transport
|
13
|
+
end
|
14
|
+
|
15
|
+
# Process a single line of input.
|
16
|
+
# @param line [String, Hash]
|
17
|
+
def call(line)
|
18
|
+
request = parse_request(line)
|
19
|
+
return unless request
|
20
|
+
|
21
|
+
process_request(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def parse_request(line)
|
27
|
+
if line.is_a?(String)
|
28
|
+
line.strip!
|
29
|
+
return if line.empty?
|
30
|
+
|
31
|
+
begin
|
32
|
+
MultiJson.load(line)
|
33
|
+
rescue MultiJson::ParseError => e
|
34
|
+
Rails.logger.error("Failed to parse JSON: #{e.message}")
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
else
|
38
|
+
line
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param request [Hash]
|
43
|
+
def process_request(request)
|
44
|
+
unless request["jsonrpc"] == "2.0"
|
45
|
+
puts "Invalid request: #{request}"
|
46
|
+
return
|
47
|
+
end
|
48
|
+
read(request)
|
49
|
+
return if request["error"]
|
50
|
+
return if request["result"] == {} # Probably a pong
|
51
|
+
|
52
|
+
rpc_method = request["method"]
|
53
|
+
id = request["id"]
|
54
|
+
params = request["params"]
|
55
|
+
|
56
|
+
# Common methods (both directions)
|
57
|
+
case rpc_method
|
58
|
+
when "ping" # [BOTH] Ping message
|
59
|
+
transport.send_pong(id)
|
60
|
+
when "initialize" # [BOTH] Initialization
|
61
|
+
handle_initialize(id, params)
|
62
|
+
when %r{^notifications/}
|
63
|
+
process_common_notifications(rpc_method, params)
|
64
|
+
else
|
65
|
+
handle_specific_method(rpc_method, id, params)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Override in subclasses
|
70
|
+
def handle_initialize(id, params)
|
71
|
+
raise NotImplementedError, "Subclasses must implement #handle_initialize"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Override in subclasses
|
75
|
+
def handle_specific_method(rpc_method, id, params)
|
76
|
+
raise NotImplementedError, "Subclasses must implement #handle_specific_method"
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_common_notifications(rpc_method, params)
|
80
|
+
case rpc_method
|
81
|
+
when "notifications/initialized" # [BOTH] Initialization complete
|
82
|
+
puts "Initialized"
|
83
|
+
transport.initialize!
|
84
|
+
when "notifications/cancelled" # [BOTH] Request cancellation
|
85
|
+
puts "Request #{params['requestId']} cancelled: #{params['reason']}"
|
86
|
+
# Handle cancellation
|
87
|
+
else
|
88
|
+
handle_specific_notification(rpc_method, params)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Override in subclasses
|
93
|
+
def handle_specific_notification(rpc_method, params)
|
94
|
+
raise NotImplementedError, "Subclasses must implement #handle_specific_notification"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/callbacks"
|
4
|
+
require "active_support/core_ext/module/attribute_accessors"
|
5
|
+
|
6
|
+
module ActionMCP
|
7
|
+
# = Action MCP \Callbacks
|
8
|
+
#
|
9
|
+
# Action MCP provides hooks during the life cycle of a message, command, or process.
|
10
|
+
# Callbacks allow you to trigger logic during this cycle. Available callbacks are:
|
11
|
+
#
|
12
|
+
# * <tt>before_perform</tt>
|
13
|
+
# * <tt>around_perform</tt>
|
14
|
+
# * <tt>after_perform</tt>
|
15
|
+
module Callbacks
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
include ActiveSupport::Callbacks
|
18
|
+
|
19
|
+
class << self
|
20
|
+
include ActiveSupport::Callbacks
|
21
|
+
define_callbacks :execute
|
22
|
+
end
|
23
|
+
|
24
|
+
included do
|
25
|
+
define_callbacks :perform, skip_after_callbacks_if_terminated: true
|
26
|
+
end
|
27
|
+
|
28
|
+
# These methods will be included into any Action MCP capability, adding
|
29
|
+
# callbacks for the +perform+ method.
|
30
|
+
class_methods do
|
31
|
+
# Defines a callback that will get called right before the
|
32
|
+
# object's perform method is executed.
|
33
|
+
#
|
34
|
+
# class AnalyzeCsvTool < ApplicationMCPTool
|
35
|
+
# description "Analyze a CSV file"
|
36
|
+
#
|
37
|
+
# property :filepath, type: "string", description: "Path to CSV file"
|
38
|
+
# collection :operations, type: "string", description: "Operations to perform"
|
39
|
+
#
|
40
|
+
# validates :operations, inclusion: { in: %w[sum average count] }
|
41
|
+
#
|
42
|
+
# before_perform do |mcp|
|
43
|
+
# Rails.logger.info("Starting CSV analysis for: #{mcp.filepath}")
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# def perform
|
47
|
+
# result = operations.to_h { |op| [ op, rand(1..100) ] }
|
48
|
+
# render text: result.to_json
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
def before_perform(*filters, &blk)
|
53
|
+
set_callback(:perform, :before, *filters, &blk)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Defines a callback that will get called right after the
|
57
|
+
# object's perform method has finished.
|
58
|
+
#
|
59
|
+
# class GreetingPrompt < ApplicationMCPPrompt
|
60
|
+
# description "Generates a personalized greeting message"
|
61
|
+
#
|
62
|
+
# argument :name, description: "The name to greet", required: true
|
63
|
+
# argument :style, description: "Style of greeting", enum: %w[formal casual friendly], default: "friendly"
|
64
|
+
#
|
65
|
+
# after_perform do |mcp|
|
66
|
+
# Rails.logger.info("Generated #{mcp.style} greeting for #{mcp.name}")
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# def perform
|
70
|
+
# render text: "Please create a greeting for #{name}"
|
71
|
+
# render text: "I'd be happy to create a #{style} greeting for #{name}!", role: "assistant"
|
72
|
+
# render text: "The greeting should be in #{style} style."
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
def after_perform(*filters, &blk)
|
77
|
+
set_callback(:perform, :after, *filters, &blk)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Defines a callback that will get called around the object's perform method.
|
81
|
+
#
|
82
|
+
# class AnalyzeCsvTool < ApplicationMCPTool
|
83
|
+
# description "Analyze a CSV file"
|
84
|
+
#
|
85
|
+
# property :filepath, type: "string", description: "Path to CSV file"
|
86
|
+
# collection :operations, type: "string", description: "Operations to perform"
|
87
|
+
#
|
88
|
+
# validates :operations, inclusion: { in: %w[sum average count] }
|
89
|
+
#
|
90
|
+
# around_perform do |mcp, block|
|
91
|
+
# start_time = Time.current
|
92
|
+
# Rails.logger.info("Starting CSV analysis for: #{mcp.filepath}")
|
93
|
+
# block.call
|
94
|
+
# duration = Time.current - start_time
|
95
|
+
# Rails.logger.info("Completed CSV analysis in #{duration}s")
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# def perform
|
99
|
+
# result = operations.to_h { |op| [ op, rand(1..100) ] }
|
100
|
+
# render text: result.to_json
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# You can access the return value of the perform only if the execution wasn't halted.
|
105
|
+
#
|
106
|
+
# class GreetingPrompt < ApplicationMCPPrompt
|
107
|
+
# around_perform do |mcp, block|
|
108
|
+
# value = block.call
|
109
|
+
# puts value # => Result of render operations
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def perform
|
113
|
+
# render text: "Hello #{name}!"
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
def around_perform(*filters, &blk)
|
118
|
+
set_callback(:perform, :around, *filters, &blk)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -6,6 +6,9 @@ module ActionMCP
|
|
6
6
|
class Capability
|
7
7
|
include ActiveModel::Model
|
8
8
|
include ActiveModel::Attributes
|
9
|
+
include Callbacks
|
10
|
+
include Instrumentation::Instrumentation
|
11
|
+
include Logging
|
9
12
|
include Renderable
|
10
13
|
|
11
14
|
class_attribute :_capability_name, instance_accessor: false
|
@@ -17,11 +20,11 @@ module ActionMCP
|
|
17
20
|
end
|
18
21
|
|
19
22
|
def self.abstract_capability
|
20
|
-
@
|
23
|
+
@abstract_capability ||= false # Default to false, unique to each class
|
21
24
|
end
|
22
25
|
|
23
26
|
def self.abstract_capability=(value)
|
24
|
-
@
|
27
|
+
@abstract_capability = value
|
25
28
|
end
|
26
29
|
|
27
30
|
# Marks this tool as abstract so that it won’t be available for use.
|
@@ -39,7 +42,6 @@ module ActionMCP
|
|
39
42
|
abstract_capability
|
40
43
|
end
|
41
44
|
|
42
|
-
|
43
45
|
def self.description(text = nil)
|
44
46
|
if text
|
45
47
|
self._description = text
|
@@ -47,5 +49,6 @@ module ActionMCP
|
|
47
49
|
_description
|
48
50
|
end
|
49
51
|
end
|
52
|
+
ActiveSupport.run_load_hooks(:active_mcp, self)
|
50
53
|
end
|
51
54
|
end
|
data/lib/action_mcp/client.rb
CHANGED
@@ -5,8 +5,8 @@ module ActionMCP
|
|
5
5
|
# @param endpoint [String] The endpoint to connect to (URL or command)
|
6
6
|
# @param logger [Logger] The logger to use
|
7
7
|
# @return [Client] An SSEClient or StdioClient depending on the endpoint
|
8
|
-
def self.create_client(endpoint, logger: Logger.new(
|
9
|
-
if endpoint =~
|
8
|
+
def self.create_client(endpoint, logger: Logger.new($stdout))
|
9
|
+
if endpoint =~ %r{\Ahttps?://}
|
10
10
|
logger.info("Creating SSE client for endpoint: #{endpoint}")
|
11
11
|
SSEClient.new(endpoint, logger: logger)
|
12
12
|
else
|
@@ -19,7 +19,7 @@ module ActionMCP
|
|
19
19
|
class Client
|
20
20
|
attr_reader :logger, :capabilities, :type, :connection_error
|
21
21
|
|
22
|
-
def initialize(logger: Logger.new(
|
22
|
+
def initialize(logger: Logger.new($stdout))
|
23
23
|
@logger = logger
|
24
24
|
@connected = false
|
25
25
|
@initialize_request_id = SecureRandom.uuid_v7
|
@@ -47,7 +47,7 @@ module ActionMCP
|
|
47
47
|
@connected = true
|
48
48
|
logger.info("Connected to MCP server")
|
49
49
|
true
|
50
|
-
rescue => e
|
50
|
+
rescue StandardError => e
|
51
51
|
@connection_error = e.message
|
52
52
|
logger.error("Failed to connect to MCP server: #{e.message}")
|
53
53
|
false
|
@@ -64,7 +64,7 @@ module ActionMCP
|
|
64
64
|
@connected = false
|
65
65
|
logger.info("Disconnected from MCP server")
|
66
66
|
true
|
67
|
-
rescue => e
|
67
|
+
rescue StandardError => e
|
68
68
|
logger.error("Error disconnecting from MCP server: #{e.message}")
|
69
69
|
false
|
70
70
|
end
|
@@ -83,7 +83,7 @@ module ActionMCP
|
|
83
83
|
json = prepare_payload(payload)
|
84
84
|
send_message(json)
|
85
85
|
true
|
86
|
-
rescue => e
|
86
|
+
rescue StandardError => e
|
87
87
|
logger.error("Failed to send request: #{e.message}")
|
88
88
|
false
|
89
89
|
end
|
@@ -111,9 +111,7 @@ module ActionMCP
|
|
111
111
|
|
112
112
|
# Get the server capabilities
|
113
113
|
# @return [Hash, nil] The server capabilities, or nil if not connected
|
114
|
-
|
115
|
-
@server_capabilities
|
116
|
-
end
|
114
|
+
attr_reader :server_capabilities
|
117
115
|
|
118
116
|
protected
|
119
117
|
|
@@ -159,7 +157,7 @@ module ActionMCP
|
|
159
157
|
# Initialize an SSE client
|
160
158
|
# @param endpoint [String] The SSE endpoint URL
|
161
159
|
# @param logger [Logger] The logger to use
|
162
|
-
def initialize(endpoint, logger: Logger.new(
|
160
|
+
def initialize(endpoint, logger: Logger.new($stdout))
|
163
161
|
super(logger: logger)
|
164
162
|
@endpoint = endpoint
|
165
163
|
@transport = Transport::SSEClient.new(endpoint, logger: logger)
|
@@ -172,18 +170,16 @@ module ActionMCP
|
|
172
170
|
protected
|
173
171
|
|
174
172
|
def start_transport
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
false
|
186
|
-
end
|
173
|
+
@transport.start(@initialize_request_id)
|
174
|
+
true
|
175
|
+
rescue Transport::SSEClient::ConnectionError => e
|
176
|
+
@connection_error = e.message
|
177
|
+
@error_callback&.call(e)
|
178
|
+
false
|
179
|
+
rescue StandardError => e
|
180
|
+
@connection_error = e.message
|
181
|
+
@error_callback&.call(e)
|
182
|
+
false
|
187
183
|
end
|
188
184
|
|
189
185
|
private
|
@@ -211,7 +207,7 @@ module ActionMCP
|
|
211
207
|
# Initialize a STDIO client
|
212
208
|
# @param command [String] The command to execute
|
213
209
|
# @param logger [Logger] The logger to use
|
214
|
-
def initialize(command, logger: Logger.new(
|
210
|
+
def initialize(command, logger: Logger.new($stdout))
|
215
211
|
super(logger: logger)
|
216
212
|
@command = command
|
217
213
|
@transport = Transport::StdioClient.new(command, logger: logger)
|
@@ -234,9 +230,7 @@ module ActionMCP
|
|
234
230
|
def setup_callbacks
|
235
231
|
@transport.on_message do |message|
|
236
232
|
# Check if this is a response to our initialize request
|
237
|
-
|
238
|
-
@transport.handle_initialize_response(message)
|
239
|
-
end
|
233
|
+
@transport.handle_initialize_response(message) if message&.id && message.id == @initialize_request_id
|
240
234
|
|
241
235
|
@message_callback&.call(message)
|
242
236
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Handler for client-side requests (server -> client)
|
5
|
+
class ClientJsonRpcHandler < BaseJsonRpcHandler
|
6
|
+
def handle_initialize(id, params)
|
7
|
+
# Client-specific initialization
|
8
|
+
transport.send_client_capabilities(id, params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_specific_method(rpc_method, id, params)
|
12
|
+
case rpc_method
|
13
|
+
when "client/setLoggingLevel" # [CLIENT] Server configuring client logging
|
14
|
+
transport.set_client_logging_level(id, params["level"])
|
15
|
+
when %r{^roots/} # [CLIENT] Roots management
|
16
|
+
process_roots(rpc_method, id, params)
|
17
|
+
when %r{^sampling/} # [CLIENT] Sampling requests
|
18
|
+
process_sampling(rpc_method, id, params)
|
19
|
+
else
|
20
|
+
Rails.logger.warn("Unknown client method: #{rpc_method}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle_specific_notification(rpc_method, params)
|
25
|
+
case rpc_method
|
26
|
+
when "notifications/resources/updated" # [CLIENT] Resource update notification
|
27
|
+
puts "Resource #{params['uri']} was updated"
|
28
|
+
# Handle resource update notification
|
29
|
+
when "notifications/tools/list_changed" # [CLIENT] Tool list change notification
|
30
|
+
puts "Tool list has changed"
|
31
|
+
# Handle tool list change notification
|
32
|
+
when "notifications/prompts/list_changed" # [CLIENT] Prompt list change notification
|
33
|
+
puts "Prompt list has changed"
|
34
|
+
# Handle prompt list change notification
|
35
|
+
when "notifications/resources/list_changed" # [CLIENT] Resource list change notification
|
36
|
+
puts "Resource list has changed"
|
37
|
+
# Handle resource list change notification
|
38
|
+
else
|
39
|
+
Rails.logger.warn("Unknown client notification: #{rpc_method}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @param rpc_method [String]
|
46
|
+
# @param id [String]
|
47
|
+
# @param params [Hash]
|
48
|
+
def process_roots(rpc_method, id, params)
|
49
|
+
case rpc_method
|
50
|
+
when "roots/list" # [CLIENT] List available roots
|
51
|
+
transport.send_roots_list(id)
|
52
|
+
else
|
53
|
+
Rails.logger.warn("Unknown roots method: #{rpc_method}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param rpc_method [String]
|
58
|
+
# @param id [String]
|
59
|
+
# @param params [Hash]
|
60
|
+
def process_sampling(rpc_method, id, params)
|
61
|
+
case rpc_method
|
62
|
+
when "sampling/createMessage" # [CLIENT] Create a message using AI
|
63
|
+
transport.send_sampling_create_message(id, params)
|
64
|
+
else
|
65
|
+
Rails.logger.warn("Unknown sampling method: #{rpc_method}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -25,7 +25,6 @@ module ActionMCP
|
|
25
25
|
#
|
26
26
|
# @return [void]
|
27
27
|
|
28
|
-
|
29
28
|
def initialize
|
30
29
|
@logging_enabled = true
|
31
30
|
@list_changed = false
|
@@ -54,20 +53,21 @@ module ActionMCP
|
|
54
53
|
capabilities[:resources] = {} if ResourceTemplatesRegistry.non_abstract.any?
|
55
54
|
capabilities
|
56
55
|
end
|
56
|
+
|
57
57
|
private
|
58
|
+
|
58
59
|
def has_rails_version
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
false
|
65
|
-
end
|
60
|
+
gem "rails_app_version"
|
61
|
+
require "rails_app_version/railtie"
|
62
|
+
true
|
63
|
+
rescue LoadError
|
64
|
+
false
|
66
65
|
end
|
67
66
|
end
|
68
67
|
|
69
68
|
class << self
|
70
69
|
attr_accessor :server
|
70
|
+
|
71
71
|
# Returns the configuration instance.
|
72
72
|
#
|
73
73
|
# @return [Configuration] the configuration instance
|
@@ -17,7 +17,7 @@ module ActionMCP
|
|
17
17
|
# @param mime_type [String] The MIME type of the resource.
|
18
18
|
# @param text [String, nil] The text content of the resource (optional).
|
19
19
|
# @param blob [String, nil] The base64-encoded blob of the resource (optional).
|
20
|
-
def initialize(uri, mime_type, text: nil, blob: nil)
|
20
|
+
def initialize(uri, mime_type = "text/plain", text: nil, blob: nil)
|
21
21
|
super("resource")
|
22
22
|
@uri = uri
|
23
23
|
@mime_type = mime_type
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/attr_internal"
|
4
|
+
|
5
|
+
module ActionMCP
|
6
|
+
module Instrumentation
|
7
|
+
module ControllerRuntime
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
attr_internal :mcp_runtime
|
13
|
+
|
14
|
+
def cleanup_view_runtime
|
15
|
+
mcp_rt_before_render = LogSubscriber.reset_runtime
|
16
|
+
runtime = super
|
17
|
+
mcp_rt_after_render = LogSubscriber.reset_runtime
|
18
|
+
self.mcp_runtime = mcp_rt_before_render + mcp_rt_after_render
|
19
|
+
runtime - mcp_rt_after_render
|
20
|
+
end
|
21
|
+
|
22
|
+
def append_info_to_payload(payload)
|
23
|
+
super
|
24
|
+
payload[:mcp_runtime] = (mcp_runtime || 0) + LogSubscriber.reset_runtime
|
25
|
+
end
|
26
|
+
|
27
|
+
class_methods do
|
28
|
+
def log_process_action(payload)
|
29
|
+
messages = super
|
30
|
+
mcp_runtime = payload[:mcp_runtime]
|
31
|
+
messages << ("mcp: %.1fms" % mcp_runtime.to_f) if mcp_runtime
|
32
|
+
messages
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActionMCP
|
2
|
+
module Instrumentation
|
3
|
+
module Instrumentation # :nodoc:
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
around_perform do |_, block|
|
8
|
+
instrument(:perform, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def instrument(operation, payload = {}, &block)
|
15
|
+
payload[:mcp] = self
|
16
|
+
|
17
|
+
# Include type information (tool/prompt)
|
18
|
+
payload[:type] = self.class.type
|
19
|
+
|
20
|
+
ActiveSupport::Notifications.instrument("#{operation}.action_mcp", payload) do
|
21
|
+
block.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Instrumentation
|
5
|
+
module ResourceInstrumentation # :nodoc:
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
around_resolve do |_, block|
|
10
|
+
instrument(:resolve, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def instrument(operation, payload = {}, &block)
|
16
|
+
payload[:resource_template] = self
|
17
|
+
payload[:uri_template] = uri_template if respond_to?(:uri_template)
|
18
|
+
payload[:mime_type] = mime_type if respond_to?(:mime_type)
|
19
|
+
|
20
|
+
ActiveSupport::Notifications.instrument("#{operation}.action_mcp_resource", payload) do
|
21
|
+
value = block.call if block
|
22
|
+
if value
|
23
|
+
payload[:success] = true
|
24
|
+
payload[:resource] = value
|
25
|
+
else
|
26
|
+
payload[:success] = false
|
27
|
+
end
|
28
|
+
payload[:aborted] = @_halted_callback_hook_called if defined?(@_halted_callback_hook_called)
|
29
|
+
@_halted_callback_hook_called = nil
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def halted_callback_hook(*)
|
35
|
+
super
|
36
|
+
@_halted_callback_hook_called = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -55,8 +55,24 @@ module ActionMCP
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def transform_value_to_hash!(result, error)
|
58
|
-
result = result.is_a?(String)
|
59
|
-
|
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
|
60
76
|
[ result, error ]
|
61
77
|
end
|
62
78
|
end
|