actionmcp 0.2.0 → 0.2.4
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 +133 -30
- data/Rakefile +0 -2
- data/app/controllers/action_mcp/application_controller.rb +13 -0
- data/app/controllers/action_mcp/messages_controller.rb +51 -0
- data/app/controllers/action_mcp/sse_controller.rb +151 -0
- data/config/routes.rb +4 -0
- data/exe/actionmcp_cli +221 -0
- data/lib/action_mcp/capability.rb +52 -0
- data/lib/action_mcp/client.rb +243 -1
- data/lib/action_mcp/configuration.rb +50 -1
- data/lib/action_mcp/content/audio.rb +9 -0
- data/lib/action_mcp/content/image.rb +9 -0
- data/lib/action_mcp/content/resource.rb +13 -0
- data/lib/action_mcp/content/text.rb +7 -0
- data/lib/action_mcp/content.rb +11 -6
- data/lib/action_mcp/engine.rb +34 -0
- data/lib/action_mcp/gem_version.rb +2 -2
- data/lib/action_mcp/integer_array.rb +6 -0
- data/lib/action_mcp/json_rpc/json_rpc_error.rb +21 -0
- data/lib/action_mcp/json_rpc/notification.rb +8 -0
- data/lib/action_mcp/json_rpc/request.rb +14 -0
- data/lib/action_mcp/json_rpc/response.rb +32 -1
- data/lib/action_mcp/json_rpc.rb +1 -6
- data/lib/action_mcp/json_rpc_handler.rb +106 -0
- data/lib/action_mcp/logging.rb +19 -0
- data/lib/action_mcp/prompt.rb +30 -46
- data/lib/action_mcp/prompts_registry.rb +13 -1
- data/lib/action_mcp/registry_base.rb +47 -28
- data/lib/action_mcp/renderable.rb +26 -0
- data/lib/action_mcp/resource.rb +3 -1
- data/lib/action_mcp/server.rb +4 -1
- data/lib/action_mcp/string_array.rb +5 -0
- data/lib/action_mcp/tool.rb +16 -53
- data/lib/action_mcp/tools_registry.rb +14 -1
- data/lib/action_mcp/transport/capabilities.rb +21 -0
- data/lib/action_mcp/transport/messaging.rb +20 -0
- data/lib/action_mcp/transport/prompts.rb +19 -0
- data/lib/action_mcp/transport/sse_client.rb +309 -0
- data/lib/action_mcp/transport/stdio_client.rb +117 -0
- data/lib/action_mcp/transport/tools.rb +20 -0
- data/lib/action_mcp/transport/transport_base.rb +125 -0
- data/lib/action_mcp/transport.rb +1 -235
- data/lib/action_mcp/transport_handler.rb +54 -0
- data/lib/action_mcp/version.rb +4 -5
- data/lib/action_mcp.rb +36 -33
- data/lib/generators/action_mcp/prompt/templates/prompt.rb.erb +3 -1
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +5 -1
- data/lib/tasks/action_mcp_tasks.rake +28 -5
- metadata +66 -9
- data/exe/action_mcp_stdio +0 -0
- data/lib/action_mcp/railtie.rb +0 -27
- data/lib/action_mcp/resources_bank.rb +0 -94
data/exe/actionmcp_cli
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup' # Ensure correct gem dependencies
|
5
|
+
require 'optparse'
|
6
|
+
require 'multi_json'
|
7
|
+
require 'actionmcp'
|
8
|
+
require 'action_mcp/client'
|
9
|
+
require 'securerandom'
|
10
|
+
require 'logger'
|
11
|
+
|
12
|
+
# Default options
|
13
|
+
options = {
|
14
|
+
logging_level: "INFO",
|
15
|
+
auto_initialize: true
|
16
|
+
}
|
17
|
+
|
18
|
+
# Set up logger
|
19
|
+
logger = Logger.new(STDOUT)
|
20
|
+
logger.formatter = proc do |severity, _, _, msg|
|
21
|
+
"#{severity}: #{msg}\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse command-line arguments
|
25
|
+
parser = OptionParser.new do |opts|
|
26
|
+
opts.banner = "Usage: mcp_client ENDPOINT [options]"
|
27
|
+
opts.on("-l", "--log-level LEVEL", "Set log level (DEBUG, INFO, WARN, ERROR)") do |l|
|
28
|
+
options[:logging_level] = l.upcase
|
29
|
+
logger.level = Logger.const_get(l.upcase) rescue Logger::INFO
|
30
|
+
end
|
31
|
+
opts.on("--no-auto-init", "Don't automatically initialize the connection") do
|
32
|
+
options[:auto_initialize] = false
|
33
|
+
end
|
34
|
+
opts.on("-h", "--help", "Show this help message") do
|
35
|
+
puts opts
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Extract first argument as endpoint
|
41
|
+
endpoint = ARGV.shift
|
42
|
+
|
43
|
+
# Parse remaining options
|
44
|
+
parser.parse!(ARGV)
|
45
|
+
|
46
|
+
if endpoint.nil?
|
47
|
+
puts "Error: You must provide an MCP endpoint."
|
48
|
+
puts parser
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
|
52
|
+
# Function to generate a unique request ID
|
53
|
+
def generate_request_id
|
54
|
+
SecureRandom.uuid
|
55
|
+
end
|
56
|
+
|
57
|
+
# Function to parse command shortcuts and return a Request object
|
58
|
+
def parse_command(input)
|
59
|
+
parts = input.strip.split(/\s+/)
|
60
|
+
command = parts.shift
|
61
|
+
|
62
|
+
case command
|
63
|
+
when "call_tool"
|
64
|
+
tool_name = parts.shift
|
65
|
+
return nil unless tool_name
|
66
|
+
|
67
|
+
arguments = {}
|
68
|
+
parts.each do |arg|
|
69
|
+
key, value = arg.split(":", 2)
|
70
|
+
next unless value
|
71
|
+
|
72
|
+
# Try to convert the value to appropriate type
|
73
|
+
parsed_value = case value
|
74
|
+
when /^\d+$/
|
75
|
+
value.to_i
|
76
|
+
when /^\d+\.\d+$/
|
77
|
+
value.to_f
|
78
|
+
when "true"
|
79
|
+
true
|
80
|
+
when "false"
|
81
|
+
false
|
82
|
+
when "null"
|
83
|
+
nil
|
84
|
+
else
|
85
|
+
value
|
86
|
+
end
|
87
|
+
|
88
|
+
arguments[key] = parsed_value
|
89
|
+
end
|
90
|
+
|
91
|
+
ActionMCP::JsonRpc::Request.new(
|
92
|
+
id: generate_request_id,
|
93
|
+
method: "tools/get",
|
94
|
+
params: {
|
95
|
+
"name" => tool_name,
|
96
|
+
"arguments" => arguments
|
97
|
+
}
|
98
|
+
)
|
99
|
+
when "list_tools"
|
100
|
+
ActionMCP::JsonRpc::Request.new(
|
101
|
+
id: generate_request_id,
|
102
|
+
method: "tools/list"
|
103
|
+
)
|
104
|
+
when "list_prompts"
|
105
|
+
ActionMCP::JsonRpc::Request.new(
|
106
|
+
id: generate_request_id,
|
107
|
+
method: "prompts/list"
|
108
|
+
)
|
109
|
+
else
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Help message for shortcuts
|
115
|
+
def print_help
|
116
|
+
puts "Available shortcuts:"
|
117
|
+
puts " list_tools"
|
118
|
+
puts " - Get a list of available tools"
|
119
|
+
puts " call_tool TOOL_NAME PARAM1:VALUE1 PARAM2:VALUE2 ..."
|
120
|
+
puts " - Sends a tools/get request with the specified tool and parameters"
|
121
|
+
puts " list_prompts"
|
122
|
+
puts " - Get a list of available prompts"
|
123
|
+
puts " get_prompt PROMPT_NAME PARAM1:VALUE1 PARAM2:VALUE2 ..."
|
124
|
+
puts " - Sends a prompts/get request with the specified prompt and arguments"
|
125
|
+
puts " help - Show this help message"
|
126
|
+
puts " exit - Quit the client"
|
127
|
+
puts "Otherwise, enter a raw JSON-RPC request to send directly"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Initialize and start the client
|
131
|
+
client = ActionMCP.create_client(endpoint, logger: logger)
|
132
|
+
|
133
|
+
# Start the transport
|
134
|
+
unless client.connect
|
135
|
+
error_msg = client.connection_error || "Unknown connection error"
|
136
|
+
puts "\nERROR: Failed to connect to MCP server at #{endpoint}"
|
137
|
+
puts "Reason: #{error_msg}"
|
138
|
+
puts "\nPlease check that:"
|
139
|
+
puts " 1. The server is running"
|
140
|
+
puts " 2. The endpoint URL/address is correct"
|
141
|
+
puts " 3. Any required firewall ports are open"
|
142
|
+
|
143
|
+
if endpoint =~ /\Ahttps?:\/\//
|
144
|
+
puts " 4. The URL includes the correct protocol, host, and port"
|
145
|
+
puts " For example: http://localhost:3000/action_mcp"
|
146
|
+
end
|
147
|
+
|
148
|
+
exit 1
|
149
|
+
end
|
150
|
+
|
151
|
+
Signal.trap("INT") do
|
152
|
+
puts "\nReceived Ctrl+C. Disconnecting..."
|
153
|
+
client.disconnect
|
154
|
+
puts "MCP Client stopped."
|
155
|
+
exit 0
|
156
|
+
end
|
157
|
+
|
158
|
+
# Main REPL loop
|
159
|
+
loop do
|
160
|
+
print "mcp> "
|
161
|
+
input = gets&.chomp
|
162
|
+
break unless input # Handle EOF
|
163
|
+
next if input.empty?
|
164
|
+
|
165
|
+
case input.downcase
|
166
|
+
when "exit"
|
167
|
+
break
|
168
|
+
when "help"
|
169
|
+
print_help
|
170
|
+
next
|
171
|
+
else
|
172
|
+
begin
|
173
|
+
# Check if input is a command shortcut
|
174
|
+
if input.start_with?("call_tool")
|
175
|
+
request = parse_command(input)
|
176
|
+
logger.debug("Parsed shortcut to: #{request.to_h}") if request
|
177
|
+
elsif input.start_with?("connect") || input.start_with?("initialize")
|
178
|
+
request = parse_command(input)
|
179
|
+
logger.debug("Initializing connection with: #{request.to_h}") if request
|
180
|
+
elsif input.start_with?("list_tools") || input.start_with?("list_prompts")
|
181
|
+
request = parse_command(input)
|
182
|
+
logger.debug("Requesting tool list: #{request.to_h}") if request
|
183
|
+
else
|
184
|
+
# Try parsing as JSON and creating a Request object
|
185
|
+
begin
|
186
|
+
json = MultiJson.load(input)
|
187
|
+
# Validate that the parsed JSON has the required fields
|
188
|
+
if json["method"]
|
189
|
+
request = ActionMCP::JsonRpc::Request.new(
|
190
|
+
id: json["id"] || generate_request_id,
|
191
|
+
method: json["method"],
|
192
|
+
params: json["params"]
|
193
|
+
)
|
194
|
+
else
|
195
|
+
puts "Invalid JSON-RPC request: missing 'method' field"
|
196
|
+
next
|
197
|
+
end
|
198
|
+
rescue MultiJson::ParseError => e
|
199
|
+
puts "Invalid input: not a valid command or JSON. #{e.message}"
|
200
|
+
next
|
201
|
+
rescue ActionMCP::JsonRpc::JsonRpcError => e
|
202
|
+
puts "Invalid JSON-RPC request: #{e.message}"
|
203
|
+
next
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
if request
|
208
|
+
client.send_request(request.to_h)
|
209
|
+
else
|
210
|
+
puts "Invalid command format. Type 'help' for available commands."
|
211
|
+
end
|
212
|
+
rescue StandardError => e
|
213
|
+
puts "Error: #{e.message}"
|
214
|
+
puts e.backtrace.first(5) if logger.level == Logger::DEBUG
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
puts "Disconnecting..."
|
220
|
+
client.disconnect
|
221
|
+
puts "MCP Client stopped."
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "renderable"
|
4
|
+
|
5
|
+
module ActionMCP
|
6
|
+
class Capability
|
7
|
+
include ActiveModel::Model
|
8
|
+
include ActiveModel::Attributes
|
9
|
+
include Renderable
|
10
|
+
|
11
|
+
class_attribute :_capability_name, instance_accessor: false
|
12
|
+
class_attribute :_description, instance_accessor: false, default: ""
|
13
|
+
class_attribute :abstract_tool, instance_accessor: false, default: false
|
14
|
+
|
15
|
+
# use _capability_name or default_capability_name
|
16
|
+
def self.capability_name
|
17
|
+
_capability_name || default_capability_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.abstract_capability
|
21
|
+
@abstract_tool ||= false # Default to false, unique to each class
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.abstract_capability=(value)
|
25
|
+
@abstract_tool = value
|
26
|
+
end
|
27
|
+
|
28
|
+
# Marks this tool as abstract so that it won’t be available for use.
|
29
|
+
# If the tool is registered in ToolsRegistry, it is unregistered.
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
def self.abstract!
|
33
|
+
self.abstract_capability = true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns whether this tool is abstract.
|
37
|
+
#
|
38
|
+
# @return [Boolean] true if abstract, false otherwise.
|
39
|
+
def self.abstract?
|
40
|
+
abstract_capability
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def self.description(text = nil)
|
45
|
+
if text
|
46
|
+
self._description = text
|
47
|
+
else
|
48
|
+
_description
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/action_mcp/client.rb
CHANGED
@@ -1,7 +1,249 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
## TODO: Adding this so i don't forget
|
4
3
|
module ActionMCP
|
4
|
+
# Create a client appropriate for the given endpoint
|
5
|
+
# @param endpoint [String] The endpoint to connect to (URL or command)
|
6
|
+
# @param logger [Logger] The logger to use
|
7
|
+
# @return [Client] An SSEClient or StdioClient depending on the endpoint
|
8
|
+
def self.create_client(endpoint, logger: Logger.new(STDOUT))
|
9
|
+
if endpoint =~ /\Ahttps?:\/\//
|
10
|
+
logger.info("Creating SSE client for endpoint: #{endpoint}")
|
11
|
+
SSEClient.new(endpoint, logger: logger)
|
12
|
+
else
|
13
|
+
logger.info("Creating STDIO client for command: #{endpoint}")
|
14
|
+
StdioClient.new(endpoint, logger: logger)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Base client class for MCP protocol
|
5
19
|
class Client
|
20
|
+
attr_reader :logger, :capabilities, :type, :connection_error
|
21
|
+
|
22
|
+
def initialize(logger: Logger.new(STDOUT))
|
23
|
+
@logger = logger
|
24
|
+
@connected = false
|
25
|
+
@initialize_request_id = SecureRandom.uuid_v7
|
26
|
+
@server_capabilities = nil
|
27
|
+
@message_callback = nil
|
28
|
+
@error_callback = nil
|
29
|
+
@connection_error = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
return true if @connected
|
34
|
+
|
35
|
+
begin
|
36
|
+
logger.info("Connecting to MCP server...")
|
37
|
+
@connection_error = nil
|
38
|
+
|
39
|
+
# Start transport with proper error handling
|
40
|
+
success = start_transport
|
41
|
+
|
42
|
+
unless success
|
43
|
+
logger.error("Failed to establish connection to MCP server")
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
@connected = true
|
48
|
+
logger.info("Connected to MCP server")
|
49
|
+
true
|
50
|
+
rescue => e
|
51
|
+
@connection_error = e.message
|
52
|
+
logger.error("Failed to connect to MCP server: #{e.message}")
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Disconnect from the MCP server
|
58
|
+
# @return [Boolean] true if disconnection was successful
|
59
|
+
def disconnect
|
60
|
+
return true unless @connected
|
61
|
+
|
62
|
+
begin
|
63
|
+
stop_transport
|
64
|
+
@connected = false
|
65
|
+
logger.info("Disconnected from MCP server")
|
66
|
+
true
|
67
|
+
rescue => e
|
68
|
+
logger.error("Error disconnecting from MCP server: #{e.message}")
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Send a request to the MCP server
|
74
|
+
# @param payload [Hash, String] The request payload
|
75
|
+
# @return [Boolean] true if the request was sent successfully
|
76
|
+
def send_request(payload)
|
77
|
+
unless @connected
|
78
|
+
logger.error("Cannot send request - not connected")
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
json = prepare_payload(payload)
|
84
|
+
send_message(json)
|
85
|
+
true
|
86
|
+
rescue => e
|
87
|
+
logger.error("Failed to send request: #{e.message}")
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if the client is ready to send requests
|
93
|
+
# @return [Boolean] true if the client is connected and ready
|
94
|
+
def ready?
|
95
|
+
@connected && transport_ready?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set a callback for incoming messages
|
99
|
+
# @yield [message] Called when a message is received
|
100
|
+
# @yieldparam message The received message
|
101
|
+
def on_message(&block)
|
102
|
+
@message_callback = block
|
103
|
+
end
|
104
|
+
|
105
|
+
# Set a callback for errors
|
106
|
+
# @yield [error] Called when an error occurs
|
107
|
+
# @yieldparam error The error that occurred
|
108
|
+
def on_error(&block)
|
109
|
+
@error_callback = block
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get the server capabilities
|
113
|
+
# @return [Hash, nil] The server capabilities, or nil if not connected
|
114
|
+
def server_capabilities
|
115
|
+
@server_capabilities
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
# Start the transport - implemented by subclasses
|
121
|
+
def start_transport
|
122
|
+
raise NotImplementedError, "Subclasses must implement start_transport"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Stop the transport
|
126
|
+
def stop_transport
|
127
|
+
@transport.stop
|
128
|
+
end
|
129
|
+
|
130
|
+
# Send a message through the transport
|
131
|
+
def send_message(json)
|
132
|
+
@transport.send_message(json)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check if the transport is ready
|
136
|
+
def transport_ready?
|
137
|
+
@transport.ready?
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
# Prepare a payload for sending
|
143
|
+
# @param payload [Hash, String] The payload to prepare
|
144
|
+
# @return [String] The JSON-encoded payload
|
145
|
+
def prepare_payload(payload)
|
146
|
+
case payload
|
147
|
+
when String
|
148
|
+
# Assume it's already JSON
|
149
|
+
payload
|
150
|
+
else
|
151
|
+
# Try to convert to JSON
|
152
|
+
MultiJson.dump(payload)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# MCP client using Server-Sent Events (SSE) transport
|
158
|
+
class SSEClient < Client
|
159
|
+
# Initialize an SSE client
|
160
|
+
# @param endpoint [String] The SSE endpoint URL
|
161
|
+
# @param logger [Logger] The logger to use
|
162
|
+
def initialize(endpoint, logger: Logger.new(STDOUT))
|
163
|
+
super(logger: logger)
|
164
|
+
@endpoint = endpoint
|
165
|
+
@transport = Transport::SSEClient.new(endpoint, logger: logger)
|
166
|
+
@type = :sse
|
167
|
+
|
168
|
+
# Set up callbacks after transport is initialized
|
169
|
+
setup_callbacks
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
def start_transport
|
175
|
+
begin
|
176
|
+
@transport.start(@initialize_request_id)
|
177
|
+
true
|
178
|
+
rescue Transport::SSEClient::ConnectionError => e
|
179
|
+
@connection_error = e.message
|
180
|
+
@error_callback&.call(e)
|
181
|
+
false
|
182
|
+
rescue => e
|
183
|
+
@connection_error = e.message
|
184
|
+
@error_callback&.call(e)
|
185
|
+
false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def setup_callbacks
|
192
|
+
@transport.on_message do |message|
|
193
|
+
# Check if this is a response to our initialize request
|
194
|
+
puts @initialize_request_id
|
195
|
+
if message&.id == @initialize_request_id
|
196
|
+
@transport.handle_initialize_response(message)
|
197
|
+
else
|
198
|
+
puts "\e[32mCalling message callback\e[0m"
|
199
|
+
@message_callback&.call(message)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
@transport.on_error do |error|
|
204
|
+
@error_callback&.call(error)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# MCP client using Standard I/O (STDIO) transport
|
210
|
+
class StdioClient < Client
|
211
|
+
# Initialize a STDIO client
|
212
|
+
# @param command [String] The command to execute
|
213
|
+
# @param logger [Logger] The logger to use
|
214
|
+
def initialize(command, logger: Logger.new(STDOUT))
|
215
|
+
super(logger: logger)
|
216
|
+
@command = command
|
217
|
+
@transport = Transport::StdioClient.new(command, logger: logger)
|
218
|
+
@type = :stdio
|
219
|
+
|
220
|
+
# Set up callbacks after transport is initialized
|
221
|
+
setup_callbacks
|
222
|
+
end
|
223
|
+
|
224
|
+
protected
|
225
|
+
|
226
|
+
def start_transport
|
227
|
+
@transport.start
|
228
|
+
# For STDIO, we'll send the capabilities from the connect method
|
229
|
+
# after this method completes and @connected is set to true
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
def setup_callbacks
|
235
|
+
@transport.on_message do |message|
|
236
|
+
# Check if this is a response to our initialize request
|
237
|
+
if message && message.id && message.id == @initialize_request_id
|
238
|
+
@transport.handle_initialize_response(message)
|
239
|
+
end
|
240
|
+
|
241
|
+
@message_callback&.call(message)
|
242
|
+
end
|
243
|
+
|
244
|
+
@transport.on_error do |error|
|
245
|
+
@error_callback&.call(error)
|
246
|
+
end
|
247
|
+
end
|
6
248
|
end
|
7
249
|
end
|
@@ -3,18 +3,67 @@
|
|
3
3
|
module ActionMCP
|
4
4
|
# Configuration class to hold settings for the ActionMCP server.
|
5
5
|
class Configuration
|
6
|
+
# @!attribute name
|
7
|
+
# @return [String] The name of the MCP Server.
|
8
|
+
# @!attribute version
|
9
|
+
# @return [String] The version of the MCP Server.
|
10
|
+
# @!attribute logging_enabled
|
11
|
+
# @return [Boolean] Whether logging is enabled.
|
12
|
+
# @!attribute list_changed
|
13
|
+
# @return [Boolean] Whether to send a listChanged notification for tools, prompts, and resources.
|
14
|
+
# @!attribute resources_subscribe
|
15
|
+
# @return [Boolean] Whether to subscribe to resources.
|
16
|
+
# @!attribute logging_level
|
17
|
+
# @return [Symbol] The logging level.
|
6
18
|
attr_accessor :name, :version, :logging_enabled,
|
7
19
|
# Right now, if enabled, the server will send a listChanged notification for tools, prompts, and resources.
|
8
20
|
# We can make it more granular in the future, but for now, it's a simple boolean.
|
9
21
|
:list_changed,
|
10
|
-
:resources_subscribe
|
22
|
+
:resources_subscribe,
|
23
|
+
:logging_level
|
11
24
|
|
25
|
+
# Initializes a new Configuration instance.
|
26
|
+
#
|
27
|
+
# @return [void]
|
12
28
|
def initialize
|
13
29
|
# Use Rails.application values if available, or fallback to defaults.
|
14
30
|
@name = defined?(Rails) && Rails.respond_to?(:application) && Rails.application.respond_to?(:name) ? Rails.application.name : "ActionMCP"
|
15
31
|
@version = defined?(Rails) && Rails.respond_to?(:application) && Rails.application.respond_to?(:version) ? Rails.application.version.to_s.presence : "0.0.1"
|
16
32
|
@logging_enabled = true
|
17
33
|
@list_changed = false
|
34
|
+
@logging_level = :info
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a hash of capabilities.
|
38
|
+
#
|
39
|
+
# @return [Hash] A hash containing the resources capabilities.
|
40
|
+
def capabilities
|
41
|
+
capabilities = {}
|
42
|
+
# Only include each capability if the corresponding registry is non-empty.
|
43
|
+
capabilities[:tools] = { listChanged: @list_changed } if ToolsRegistry.non_abstract.any?
|
44
|
+
capabilities[:prompts] = { listChanged: @list_changed } if PromptsRegistry.non_abstract.any?
|
45
|
+
capabilities[:logging] = {} if @logging_enabled
|
46
|
+
# capabilities[:resources] = { subscribe: @resources_subscribe,
|
47
|
+
# listChanged: @list_changed }.compact
|
48
|
+
{ capabilities: capabilities }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_accessor :server
|
54
|
+
# Returns the configuration instance.
|
55
|
+
#
|
56
|
+
# @return [Configuration] the configuration instance
|
57
|
+
def configuration
|
58
|
+
@configuration ||= Configuration.new
|
59
|
+
end
|
60
|
+
|
61
|
+
# Configures the ActionMCP module.
|
62
|
+
#
|
63
|
+
# @yield [configuration] the configuration instance
|
64
|
+
# @return [void]
|
65
|
+
def configure
|
66
|
+
yield(configuration)
|
18
67
|
end
|
19
68
|
end
|
20
69
|
end
|
@@ -4,14 +4,23 @@ module ActionMCP
|
|
4
4
|
module Content
|
5
5
|
# Audio content includes a base64-encoded audio clip and its MIME type.
|
6
6
|
class Audio < Base
|
7
|
+
# @return [String] The base64-encoded audio data.
|
8
|
+
# @return [String] The MIME type of the audio data.
|
7
9
|
attr_reader :data, :mime_type
|
8
10
|
|
11
|
+
# Initializes a new Audio content.
|
12
|
+
#
|
13
|
+
# @param data [String] The base64-encoded audio data.
|
14
|
+
# @param mime_type [String] The MIME type of the audio data.
|
9
15
|
def initialize(data, mime_type)
|
10
16
|
super("audio")
|
11
17
|
@data = data
|
12
18
|
@mime_type = mime_type
|
13
19
|
end
|
14
20
|
|
21
|
+
# Returns a hash representation of the audio content.
|
22
|
+
#
|
23
|
+
# @return [Hash] The hash representation of the audio content.
|
15
24
|
def to_h
|
16
25
|
super.merge(data: @data, mimeType: @mime_type)
|
17
26
|
end
|
@@ -4,14 +4,23 @@ module ActionMCP
|
|
4
4
|
module Content
|
5
5
|
# Image content includes a base64-encoded image and its MIME type.
|
6
6
|
class Image < Base
|
7
|
+
# @return [String] The base64-encoded image data.
|
8
|
+
# @return [String] The MIME type of the image data.
|
7
9
|
attr_reader :data, :mime_type
|
8
10
|
|
11
|
+
# Initializes a new Image content.
|
12
|
+
#
|
13
|
+
# @param data [String] The base64-encoded image data.
|
14
|
+
# @param mime_type [String] The MIME type of the image data.
|
9
15
|
def initialize(data, mime_type)
|
10
16
|
super("image")
|
11
17
|
@data = data
|
12
18
|
@mime_type = mime_type
|
13
19
|
end
|
14
20
|
|
21
|
+
# Returns a hash representation of the image content.
|
22
|
+
#
|
23
|
+
# @return [Hash] The hash representation of the image content.
|
15
24
|
def to_h
|
16
25
|
super.merge(data: @data, mimeType: @mime_type)
|
17
26
|
end
|
@@ -5,8 +5,18 @@ module ActionMCP
|
|
5
5
|
# Resource content references a server-managed resource.
|
6
6
|
# It includes a URI, MIME type, and optionally text content or a base64-encoded blob.
|
7
7
|
class Resource < Base
|
8
|
+
# @return [String] The URI of the resource.
|
9
|
+
# @return [String] The MIME type of the resource.
|
10
|
+
# @return [String, nil] The text content of the resource (optional).
|
11
|
+
# @return [String, nil] The base64-encoded blob of the resource (optional).
|
8
12
|
attr_reader :uri, :mime_type, :text, :blob
|
9
13
|
|
14
|
+
# Initializes a new Resource content.
|
15
|
+
#
|
16
|
+
# @param uri [String] The URI of the resource.
|
17
|
+
# @param mime_type [String] The MIME type of the resource.
|
18
|
+
# @param text [String, nil] The text content of the resource (optional).
|
19
|
+
# @param blob [String, nil] The base64-encoded blob of the resource (optional).
|
10
20
|
def initialize(uri, mime_type, text: nil, blob: nil)
|
11
21
|
super("resource")
|
12
22
|
@uri = uri
|
@@ -15,6 +25,9 @@ module ActionMCP
|
|
15
25
|
@blob = blob
|
16
26
|
end
|
17
27
|
|
28
|
+
# Returns a hash representation of the resource content.
|
29
|
+
#
|
30
|
+
# @return [Hash] The hash representation of the resource content.
|
18
31
|
def to_h
|
19
32
|
resource_data = { uri: @uri, mimeType: @mime_type }
|
20
33
|
resource_data[:text] = @text if @text
|
@@ -4,13 +4,20 @@ module ActionMCP
|
|
4
4
|
module Content
|
5
5
|
# Text content represents plain text messages.
|
6
6
|
class Text < Base
|
7
|
+
# @return [String] The text content.
|
7
8
|
attr_reader :text
|
8
9
|
|
10
|
+
# Initializes a new Text content.
|
11
|
+
#
|
12
|
+
# @param text [String] The text content.
|
9
13
|
def initialize(text)
|
10
14
|
super("text")
|
11
15
|
@text = text.to_s
|
12
16
|
end
|
13
17
|
|
18
|
+
# Returns a hash representation of the text content.
|
19
|
+
#
|
20
|
+
# @return [Hash] The hash representation of the text content.
|
14
21
|
def to_h
|
15
22
|
super.merge(text: @text)
|
16
23
|
end
|