actionmcp 0.102.0 → 0.104.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 +46 -3
- data/app/models/action_mcp/session.rb +6 -5
- data/lib/action_mcp/configuration.rb +44 -8
- data/lib/action_mcp/server/base_session.rb +5 -1
- data/lib/action_mcp/test_helper/session_store_assertions.rb +0 -70
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +0 -1
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +4 -4
- data/lib/generators/action_mcp/install/templates/mcp.yml +11 -1
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +2 -2
- metadata +1 -26
- data/lib/action_mcp/client/active_record_session_store.rb +0 -57
- data/lib/action_mcp/client/base.rb +0 -225
- data/lib/action_mcp/client/blueprint.rb +0 -163
- data/lib/action_mcp/client/catalog.rb +0 -164
- data/lib/action_mcp/client/collection.rb +0 -168
- data/lib/action_mcp/client/elicitation.rb +0 -34
- data/lib/action_mcp/client/json_rpc_handler.rb +0 -202
- data/lib/action_mcp/client/logging.rb +0 -19
- data/lib/action_mcp/client/messaging.rb +0 -28
- data/lib/action_mcp/client/prompt_book.rb +0 -117
- data/lib/action_mcp/client/prompts.rb +0 -47
- data/lib/action_mcp/client/request_timeouts.rb +0 -74
- data/lib/action_mcp/client/resources.rb +0 -100
- data/lib/action_mcp/client/roots.rb +0 -13
- data/lib/action_mcp/client/server.rb +0 -60
- data/lib/action_mcp/client/session_store.rb +0 -39
- data/lib/action_mcp/client/session_store_factory.rb +0 -27
- data/lib/action_mcp/client/streamable_client.rb +0 -264
- data/lib/action_mcp/client/streamable_http_transport.rb +0 -306
- data/lib/action_mcp/client/test_session_store.rb +0 -84
- data/lib/action_mcp/client/toolbox.rb +0 -199
- data/lib/action_mcp/client/tools.rb +0 -47
- data/lib/action_mcp/client/transport.rb +0 -137
- data/lib/action_mcp/client/volatile_session_store.rb +0 -38
- data/lib/action_mcp/client.rb +0 -71
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "transport"
|
|
4
|
-
require_relative "session_store"
|
|
5
|
-
|
|
6
|
-
module ActionMCP
|
|
7
|
-
module Client
|
|
8
|
-
# StreamableHTTP transport implementation following MCP specification
|
|
9
|
-
class StreamableHttpTransport < TransportBase
|
|
10
|
-
class ConnectionError < StandardError; end
|
|
11
|
-
class AuthenticationError < StandardError; end
|
|
12
|
-
|
|
13
|
-
SSE_TIMEOUT = 10
|
|
14
|
-
ENDPOINT_TIMEOUT = 5
|
|
15
|
-
|
|
16
|
-
attr_reader :session_id, :last_event_id, :protocol_version
|
|
17
|
-
|
|
18
|
-
def initialize(url, session_store:, session_id: nil, protocol_version: nil, **options)
|
|
19
|
-
super(url, session_store: session_store, **options)
|
|
20
|
-
@session_id = session_id
|
|
21
|
-
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
|
22
|
-
@negotiated_protocol_version = nil
|
|
23
|
-
@last_event_id = nil
|
|
24
|
-
@buffer = +""
|
|
25
|
-
@current_event = nil
|
|
26
|
-
@reconnect_attempts = 0
|
|
27
|
-
@max_reconnect_attempts = options[:max_reconnect_attempts] || 3
|
|
28
|
-
@reconnect_delay = options[:reconnect_delay] || 1.0
|
|
29
|
-
|
|
30
|
-
setup_http_client
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def connect
|
|
34
|
-
log_debug("Connecting via StreamableHTTP to #{@url}")
|
|
35
|
-
|
|
36
|
-
# Load session if session_id provided
|
|
37
|
-
load_session_state if @session_id
|
|
38
|
-
|
|
39
|
-
# Start SSE stream if server supports it
|
|
40
|
-
start_sse_stream
|
|
41
|
-
|
|
42
|
-
# Set ready first, then connected (so transport is ready when on_connect fires)
|
|
43
|
-
set_ready(true)
|
|
44
|
-
set_connected(true)
|
|
45
|
-
log_debug("StreamableHTTP connection established")
|
|
46
|
-
true
|
|
47
|
-
rescue StandardError => e
|
|
48
|
-
handle_error(e)
|
|
49
|
-
false
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def disconnect
|
|
53
|
-
return true unless connected?
|
|
54
|
-
|
|
55
|
-
log_debug("Disconnecting StreamableHTTP")
|
|
56
|
-
stop_sse_stream
|
|
57
|
-
save_session_state if @session_id
|
|
58
|
-
set_connected(false)
|
|
59
|
-
set_ready(false)
|
|
60
|
-
true
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
handle_error(e)
|
|
63
|
-
false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def send_message(message)
|
|
67
|
-
raise ConnectionError, "Transport not ready" unless ready?
|
|
68
|
-
|
|
69
|
-
headers = build_post_headers
|
|
70
|
-
json_data = message.is_a?(String) ? message : message.to_json
|
|
71
|
-
|
|
72
|
-
log_debug("Sending message via POST")
|
|
73
|
-
response = @http_client.post(@url, json_data, headers)
|
|
74
|
-
|
|
75
|
-
handle_post_response(response, message)
|
|
76
|
-
true
|
|
77
|
-
rescue StandardError => e
|
|
78
|
-
handle_error(e)
|
|
79
|
-
false
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def setup_http_client
|
|
85
|
-
require "faraday"
|
|
86
|
-
@http_client = Faraday.new do |f|
|
|
87
|
-
f.headers["User-Agent"] = user_agent
|
|
88
|
-
f.options.timeout = nil # No read timeout for SSE
|
|
89
|
-
f.options.open_timeout = SSE_TIMEOUT
|
|
90
|
-
f.adapter :net_http
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def build_get_headers
|
|
95
|
-
headers = {
|
|
96
|
-
"Accept" => "text/event-stream",
|
|
97
|
-
"Cache-Control" => "no-cache"
|
|
98
|
-
}
|
|
99
|
-
headers["mcp-session-id"] = @session_id if @session_id
|
|
100
|
-
headers["Last-Event-ID"] = @last_event_id if @last_event_id
|
|
101
|
-
|
|
102
|
-
# Add MCP-Protocol-Version header for GET requests when we have a negotiated version
|
|
103
|
-
headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
|
|
104
|
-
|
|
105
|
-
log_debug("Final GET headers: #{headers}")
|
|
106
|
-
headers
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def build_post_headers
|
|
110
|
-
headers = {
|
|
111
|
-
"Content-Type" => "application/json",
|
|
112
|
-
"Accept" => "application/json, text/event-stream"
|
|
113
|
-
}
|
|
114
|
-
headers["mcp-session-id"] = @session_id if @session_id
|
|
115
|
-
|
|
116
|
-
# Add MCP-Protocol-Version header as per 2025-06-18 spec
|
|
117
|
-
# Only include when we have a negotiated version from previous handshake
|
|
118
|
-
headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
|
|
119
|
-
|
|
120
|
-
log_debug("Final POST headers: #{headers}")
|
|
121
|
-
headers
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def start_sse_stream
|
|
125
|
-
log_debug("Starting SSE stream")
|
|
126
|
-
@sse_thread = Thread.new { run_sse_stream }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def stop_sse_stream
|
|
130
|
-
return unless @sse_thread
|
|
131
|
-
|
|
132
|
-
log_debug("Stopping SSE stream")
|
|
133
|
-
@stop_requested = true
|
|
134
|
-
@sse_thread.kill if @sse_thread.alive?
|
|
135
|
-
@sse_thread = nil
|
|
136
|
-
@stop_requested = false
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def run_sse_stream
|
|
140
|
-
headers = build_get_headers
|
|
141
|
-
|
|
142
|
-
@http_client.get(@url, nil, headers) do |req|
|
|
143
|
-
req.options.on_data = proc do |chunk, _bytes|
|
|
144
|
-
break if @stop_requested
|
|
145
|
-
|
|
146
|
-
process_sse_chunk(chunk)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
rescue StandardError => e
|
|
150
|
-
handle_sse_error(e)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def process_sse_chunk(chunk)
|
|
154
|
-
@buffer << chunk
|
|
155
|
-
process_complete_events while @buffer.include?("\n\n")
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def process_complete_events
|
|
159
|
-
event_data, _separator, rest = @buffer.partition("\n\n")
|
|
160
|
-
@buffer = rest
|
|
161
|
-
|
|
162
|
-
return if event_data.strip.empty?
|
|
163
|
-
|
|
164
|
-
parse_sse_event(event_data)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def parse_sse_event(event_data)
|
|
168
|
-
lines = event_data.split("\n")
|
|
169
|
-
event_id = nil
|
|
170
|
-
data_lines = []
|
|
171
|
-
|
|
172
|
-
lines.each do |line|
|
|
173
|
-
if line.start_with?("id:")
|
|
174
|
-
event_id = line[3..].strip
|
|
175
|
-
elsif line.start_with?("data:")
|
|
176
|
-
data_lines << line[5..].strip
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
return if data_lines.empty?
|
|
181
|
-
|
|
182
|
-
@last_event_id = event_id if event_id
|
|
183
|
-
|
|
184
|
-
begin
|
|
185
|
-
message_data = data_lines.join("\n")
|
|
186
|
-
message = MultiJson.load(message_data)
|
|
187
|
-
handle_message(message)
|
|
188
|
-
rescue MultiJson::ParseError => e
|
|
189
|
-
log_error("Failed to parse SSE message: #{e}")
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def handle_post_response(response, _original_message)
|
|
194
|
-
# Extract session ID from response headers
|
|
195
|
-
@session_id = response.headers["mcp-session-id"] if response.headers["mcp-session-id"]
|
|
196
|
-
|
|
197
|
-
case response.status
|
|
198
|
-
when 200
|
|
199
|
-
handle_success_response(response)
|
|
200
|
-
when 202
|
|
201
|
-
# Accepted - message received, no immediate response
|
|
202
|
-
log_debug("Message accepted (202)")
|
|
203
|
-
when 401
|
|
204
|
-
raise AuthenticationError, "Authentication required"
|
|
205
|
-
when 405
|
|
206
|
-
# Method not allowed - server doesn't support this operation
|
|
207
|
-
log_debug("Server returned 405 - operation not supported")
|
|
208
|
-
else
|
|
209
|
-
handle_error_response(response)
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def handle_success_response(response)
|
|
214
|
-
content_type = response.headers["content-type"]
|
|
215
|
-
|
|
216
|
-
if content_type&.include?("application/json")
|
|
217
|
-
# Direct JSON response
|
|
218
|
-
handle_json_response(response)
|
|
219
|
-
elsif content_type&.include?("text/event-stream")
|
|
220
|
-
# SSE response stream
|
|
221
|
-
handle_sse_response_stream(response)
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def handle_json_response(response)
|
|
226
|
-
message = MultiJson.load(response.body)
|
|
227
|
-
|
|
228
|
-
# Check if this is an initialize response to capture negotiated protocol version
|
|
229
|
-
if message.is_a?(Hash) && message["result"] && message["result"]["protocolVersion"]
|
|
230
|
-
@negotiated_protocol_version = message["result"]["protocolVersion"]
|
|
231
|
-
log_debug("Negotiated protocol version: #{@negotiated_protocol_version}")
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
handle_message(message)
|
|
235
|
-
rescue MultiJson::ParseError => e
|
|
236
|
-
log_error("Failed to parse JSON response: #{e}")
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def handle_sse_response_stream(response)
|
|
240
|
-
# Handle SSE stream from POST response
|
|
241
|
-
response.body.each_line do |line|
|
|
242
|
-
process_sse_chunk(line)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def handle_error_response(response)
|
|
247
|
-
error_msg = +"HTTP #{response.status}: #{response.reason_phrase}"
|
|
248
|
-
error_msg << " - #{response.body}" if response.body && !response.body.empty?
|
|
249
|
-
raise ConnectionError, error_msg
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def handle_sse_error(error)
|
|
253
|
-
log_error("SSE stream error: #{error.message}")
|
|
254
|
-
|
|
255
|
-
if should_reconnect?
|
|
256
|
-
schedule_reconnect
|
|
257
|
-
else
|
|
258
|
-
handle_error(error)
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def should_reconnect?
|
|
263
|
-
connected? && @reconnect_attempts < @max_reconnect_attempts
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def schedule_reconnect
|
|
267
|
-
@reconnect_attempts += 1
|
|
268
|
-
delay = @reconnect_delay * @reconnect_attempts
|
|
269
|
-
|
|
270
|
-
log_debug("Scheduling SSE reconnect in #{delay}s (attempt #{@reconnect_attempts})")
|
|
271
|
-
|
|
272
|
-
Thread.new do
|
|
273
|
-
sleep(delay)
|
|
274
|
-
start_sse_stream unless @stop_requested
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def load_session_state
|
|
279
|
-
session_data = @session_store.load_session(@session_id)
|
|
280
|
-
return unless session_data
|
|
281
|
-
|
|
282
|
-
@last_event_id = session_data[:last_event_id]
|
|
283
|
-
log_debug("Loaded session state: last_event_id=#{@last_event_id}")
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def save_session_state
|
|
287
|
-
return unless @session_id
|
|
288
|
-
|
|
289
|
-
session_data = {
|
|
290
|
-
id: @session_id,
|
|
291
|
-
last_event_id: @last_event_id,
|
|
292
|
-
session_data: {},
|
|
293
|
-
protocol_version: @protocol_version
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
@session_store.save_session(@session_id, session_data)
|
|
297
|
-
log_debug("Saved session state")
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def user_agent
|
|
302
|
-
"ActionMCP-StreamableHTTP/#{ActionMCP.gem_version}"
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Test session store that tracks all operations for assertions
|
|
6
|
-
class TestSessionStore < VolatileSessionStore
|
|
7
|
-
attr_reader :operations, :saved_sessions, :loaded_sessions,
|
|
8
|
-
:deleted_sessions, :updated_sessions
|
|
9
|
-
|
|
10
|
-
def initialize
|
|
11
|
-
super
|
|
12
|
-
@operations = Concurrent::Array.new
|
|
13
|
-
@saved_sessions = Concurrent::Array.new
|
|
14
|
-
@loaded_sessions = Concurrent::Array.new
|
|
15
|
-
@deleted_sessions = Concurrent::Array.new
|
|
16
|
-
@updated_sessions = Concurrent::Array.new
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def load_session(session_id)
|
|
20
|
-
session = super
|
|
21
|
-
@operations << { type: :load, session_id: session_id, found: !session.nil? }
|
|
22
|
-
@loaded_sessions << session_id if session
|
|
23
|
-
session
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def save_session(session_id, session_data)
|
|
27
|
-
super
|
|
28
|
-
@operations << { type: :save, session_id: session_id, data: session_data }
|
|
29
|
-
@saved_sessions << session_id
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def delete_session(session_id)
|
|
33
|
-
result = super
|
|
34
|
-
@operations << { type: :delete, session_id: session_id }
|
|
35
|
-
@deleted_sessions << session_id
|
|
36
|
-
result
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def update_session(session_id, attributes)
|
|
40
|
-
result = super
|
|
41
|
-
@operations << { type: :update, session_id: session_id, attributes: attributes }
|
|
42
|
-
@updated_sessions << session_id if result
|
|
43
|
-
result
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Test helper methods
|
|
47
|
-
def session_saved?(session_id)
|
|
48
|
-
@saved_sessions.include?(session_id)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def session_loaded?(session_id)
|
|
52
|
-
@loaded_sessions.include?(session_id)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def session_deleted?(session_id)
|
|
56
|
-
@deleted_sessions.include?(session_id)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def session_updated?(session_id)
|
|
60
|
-
@updated_sessions.include?(session_id)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def operation_count(type = nil)
|
|
64
|
-
if type
|
|
65
|
-
@operations.count { |op| op[:type] == type }
|
|
66
|
-
else
|
|
67
|
-
@operations.size
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def last_saved_data(session_id)
|
|
72
|
-
@operations.reverse.find { |op| op[:type] == :save && op[:session_id] == session_id }&.dig(:data)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def reset_tracking!
|
|
76
|
-
@operations.clear
|
|
77
|
-
@saved_sessions.clear
|
|
78
|
-
@loaded_sessions.clear
|
|
79
|
-
@deleted_sessions.clear
|
|
80
|
-
@updated_sessions.clear
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Toolbox
|
|
6
|
-
#
|
|
7
|
-
# A collection that manages and provides access to tools from the server.
|
|
8
|
-
# This class stores tool definitions along with their input schemas and
|
|
9
|
-
# provides methods for retrieving, filtering, and accessing tools.
|
|
10
|
-
#
|
|
11
|
-
# Example usage:
|
|
12
|
-
# tools_data = client.list_tools # Returns array of tool definitions
|
|
13
|
-
# toolbox = Toolbox.new(tools_data)
|
|
14
|
-
#
|
|
15
|
-
# # Access a specific tool by name
|
|
16
|
-
# weather_tool = toolbox.find("weather_forecast")
|
|
17
|
-
#
|
|
18
|
-
# # Get all tools matching a criteria
|
|
19
|
-
# calculation_tools = toolbox.filter { |t| t.name.include?("calculate") }
|
|
20
|
-
#
|
|
21
|
-
class Toolbox < Collection
|
|
22
|
-
# Initialize a new Toolbox with tool definitions
|
|
23
|
-
#
|
|
24
|
-
# @param tools [Array<Hash>] Array of tool definition hashes, each containing
|
|
25
|
-
# name, description, and inputSchema keys
|
|
26
|
-
def initialize(tools, client)
|
|
27
|
-
super(tools, client)
|
|
28
|
-
self.tools = @collection_data
|
|
29
|
-
@load_method = :list_tools
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Find a tool by name
|
|
33
|
-
#
|
|
34
|
-
# @param name [String] Name of the tool to find
|
|
35
|
-
# @return [Tool, nil] The tool with the given name, or nil if not found
|
|
36
|
-
def find(name)
|
|
37
|
-
all.find { |tool| tool.name == name }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Filter tools based on a given block
|
|
41
|
-
#
|
|
42
|
-
# @yield [tool] Block that determines whether to include a tool
|
|
43
|
-
# @yieldparam tool [Tool] A tool from the collection
|
|
44
|
-
# @yieldreturn [Boolean] true to include the tool, false to exclude it
|
|
45
|
-
# @return [Array<Tool>] Tools that match the filter criteria
|
|
46
|
-
def filter(&block)
|
|
47
|
-
all.select(&block)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Get a list of all tool names
|
|
51
|
-
#
|
|
52
|
-
# @return [Array<String>] Names of all tools in the collection
|
|
53
|
-
def names
|
|
54
|
-
all.map(&:name)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Number of tools in the collection
|
|
58
|
-
#
|
|
59
|
-
# @return [Integer] The number of tools
|
|
60
|
-
def size
|
|
61
|
-
all.size
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Check if the collection contains a tool with the given name
|
|
65
|
-
#
|
|
66
|
-
# @param name [String] The tool name to check for
|
|
67
|
-
# @return [Boolean] true if a tool with the name exists
|
|
68
|
-
def contains?(name)
|
|
69
|
-
all.any? { |tool| tool.name == name }
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Get tools by category or type
|
|
73
|
-
#
|
|
74
|
-
# @param keyword [String] Keyword to search for in tool names and descriptions
|
|
75
|
-
# @return [Array<Tool>] Tools containing the keyword
|
|
76
|
-
def search(keyword)
|
|
77
|
-
all.select do |tool|
|
|
78
|
-
tool.name.include?(keyword) ||
|
|
79
|
-
tool.description&.downcase&.include?(keyword.downcase)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Generate a hash representation of all tools in the collection based on provider format
|
|
84
|
-
#
|
|
85
|
-
# @param provider [Symbol] The provider format to use (:claude, :openai, or :default)
|
|
86
|
-
# @return [Hash] Hash containing all tools formatted for the specified provider
|
|
87
|
-
def to_h(provider = :default)
|
|
88
|
-
case provider
|
|
89
|
-
when :claude
|
|
90
|
-
# Claude format
|
|
91
|
-
{ "tools" => all.map(&:to_claude_h) }
|
|
92
|
-
when :openai
|
|
93
|
-
# OpenAI format
|
|
94
|
-
{ "tools" => all.map(&:to_openai_h) }
|
|
95
|
-
else
|
|
96
|
-
# Default format (same as original)
|
|
97
|
-
{ "tools" => all.map(&:to_h) }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def tools=(tools)
|
|
102
|
-
@collection_data = tools.map { |tool_data| Tool.new(tool_data) }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Internal Tool class to represent individual tools
|
|
106
|
-
class Tool
|
|
107
|
-
attr_reader :name, :description, :input_schema, :annotations
|
|
108
|
-
|
|
109
|
-
# Initialize a new Tool instance
|
|
110
|
-
#
|
|
111
|
-
# @param data [Hash] Tool definition hash containing name, description, and inputSchema
|
|
112
|
-
# and optionally annotations
|
|
113
|
-
def initialize(data)
|
|
114
|
-
@name = data["name"]
|
|
115
|
-
@description = data["description"]
|
|
116
|
-
@input_schema = data["inputSchema"] || {}
|
|
117
|
-
@annotations = data["annotations"] || {}
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Get all required properties for this tool
|
|
121
|
-
#
|
|
122
|
-
# @return [Array<String>] Array of required property names
|
|
123
|
-
def required_properties
|
|
124
|
-
@input_schema["required"] || []
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Get all properties for this tool
|
|
128
|
-
#
|
|
129
|
-
# @return [Hash] Hash of property definitions
|
|
130
|
-
def properties
|
|
131
|
-
@input_schema["properties"] || {}
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Check if the tool requires a specific property
|
|
135
|
-
#
|
|
136
|
-
# @param name [String] Name of the property to check
|
|
137
|
-
# @return [Boolean] true if the property is required
|
|
138
|
-
def requires?(name)
|
|
139
|
-
required_properties.include?(name)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Check if the tool has a specific property
|
|
143
|
-
#
|
|
144
|
-
# @param name [String] Name of the property to check
|
|
145
|
-
# @return [Boolean] true if the property exists
|
|
146
|
-
def has_property?(name)
|
|
147
|
-
properties.key?(name)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Get property details by name
|
|
151
|
-
#
|
|
152
|
-
# @param name [String] Name of the property
|
|
153
|
-
# @return [Hash, nil] Property details or nil if not found
|
|
154
|
-
def property(name)
|
|
155
|
-
properties[name]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Generate a hash representation of the tool (default format)
|
|
159
|
-
#
|
|
160
|
-
# @return [Hash] Hash containing tool details
|
|
161
|
-
def to_h
|
|
162
|
-
{
|
|
163
|
-
"name" => @name,
|
|
164
|
-
"description" => @description,
|
|
165
|
-
"inputSchema" => @input_schema,
|
|
166
|
-
"annotations" => @annotations
|
|
167
|
-
}
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Generate a hash representation of the tool in Claude format
|
|
171
|
-
#
|
|
172
|
-
# @return [Hash] Hash containing tool details formatted for Claude
|
|
173
|
-
def to_claude_h
|
|
174
|
-
{
|
|
175
|
-
"name" => @name,
|
|
176
|
-
"description" => @description,
|
|
177
|
-
"input_schema" => @input_schema.transform_keys { |k| k == "inputSchema" ? "input_schema" : k },
|
|
178
|
-
"annotations" => @annotations
|
|
179
|
-
}
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Generate a hash representation of the tool in OpenAI format
|
|
183
|
-
#
|
|
184
|
-
# @return [Hash] Hash containing tool details formatted for OpenAI
|
|
185
|
-
def to_openai_h
|
|
186
|
-
{
|
|
187
|
-
"type" => "function",
|
|
188
|
-
"function" => {
|
|
189
|
-
"name" => @name,
|
|
190
|
-
"description" => @description,
|
|
191
|
-
"parameters" => @input_schema,
|
|
192
|
-
"annotations" => @annotations
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
module Tools
|
|
6
|
-
# List all available tools from the server
|
|
7
|
-
# @param params [Hash] Optional parameters for pagination
|
|
8
|
-
# @option params [String] :cursor Pagination cursor for fetching next page
|
|
9
|
-
# @option params [Integer] :limit Maximum number of items to return
|
|
10
|
-
# @return [String] Request ID for tracking the request
|
|
11
|
-
def list_tools(params = {})
|
|
12
|
-
request_id = SecureRandom.uuid_v7
|
|
13
|
-
|
|
14
|
-
# Send request with pagination parameters if provided
|
|
15
|
-
request_params = {}
|
|
16
|
-
request_params[:cursor] = params[:cursor] if params[:cursor]
|
|
17
|
-
request_params[:limit] = params[:limit] if params[:limit]
|
|
18
|
-
|
|
19
|
-
send_jsonrpc_request("tools/list",
|
|
20
|
-
params: request_params.empty? ? nil : request_params,
|
|
21
|
-
id: request_id)
|
|
22
|
-
|
|
23
|
-
# Return request ID for timeout tracking
|
|
24
|
-
request_id
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Call a specific tool on the server
|
|
28
|
-
# @param name [String] Name of the tool to call
|
|
29
|
-
# @param arguments [Hash] Arguments to pass to the tool
|
|
30
|
-
# @return [String] Request ID for tracking the request
|
|
31
|
-
def call_tool(name, arguments)
|
|
32
|
-
request_id = SecureRandom.uuid_v7
|
|
33
|
-
|
|
34
|
-
# Send request
|
|
35
|
-
send_jsonrpc_request("tools/call",
|
|
36
|
-
params: {
|
|
37
|
-
name: name,
|
|
38
|
-
arguments: arguments
|
|
39
|
-
},
|
|
40
|
-
id: request_id)
|
|
41
|
-
|
|
42
|
-
# Return request ID for tracking the request
|
|
43
|
-
request_id
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|