actionmcp 0.20.0 → 0.24.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/action_mcp/messages_controller.rb +2 -2
  3. data/app/models/action_mcp/session/message.rb +21 -5
  4. data/app/models/action_mcp/session.rb +8 -4
  5. data/lib/action_mcp/capability.rb +2 -3
  6. data/lib/action_mcp/client/base.rb +222 -0
  7. data/lib/action_mcp/client/blueprint.rb +161 -0
  8. data/lib/action_mcp/client/catalog.rb +160 -0
  9. data/lib/action_mcp/client/collection.rb +93 -0
  10. data/lib/action_mcp/client/json_rpc_handler.rb +114 -0
  11. data/lib/action_mcp/client/logging.rb +20 -0
  12. data/lib/action_mcp/{transport → client}/messaging.rb +1 -1
  13. data/lib/action_mcp/client/prompt_book.rb +117 -0
  14. data/lib/action_mcp/client/prompts.rb +39 -0
  15. data/lib/action_mcp/client/request_timeouts.rb +76 -0
  16. data/lib/action_mcp/client/resources.rb +85 -0
  17. data/lib/action_mcp/client/roots.rb +13 -0
  18. data/lib/action_mcp/client/server.rb +60 -0
  19. data/lib/action_mcp/{transport → client}/sse_client.rb +70 -111
  20. data/lib/action_mcp/{transport → client}/stdio_client.rb +38 -38
  21. data/lib/action_mcp/client/toolbox.rb +194 -0
  22. data/lib/action_mcp/client/tools.rb +39 -0
  23. data/lib/action_mcp/client.rb +20 -231
  24. data/lib/action_mcp/engine.rb +1 -3
  25. data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
  26. data/lib/action_mcp/instrumentation/instrumentation.rb +2 -0
  27. data/lib/action_mcp/instrumentation/resource_instrumentation.rb +1 -0
  28. data/lib/action_mcp/json_rpc_handler_base.rb +106 -0
  29. data/lib/action_mcp/log_subscriber.rb +2 -0
  30. data/lib/action_mcp/logging.rb +1 -1
  31. data/lib/action_mcp/{transport → server}/capabilities.rb +2 -2
  32. data/lib/action_mcp/server/json_rpc_handler.rb +121 -0
  33. data/lib/action_mcp/server/messaging.rb +28 -0
  34. data/lib/action_mcp/{transport → server}/notifications.rb +1 -1
  35. data/lib/action_mcp/{transport → server}/prompts.rb +1 -1
  36. data/lib/action_mcp/{transport → server}/resources.rb +1 -18
  37. data/lib/action_mcp/{transport → server}/roots.rb +1 -1
  38. data/lib/action_mcp/{transport → server}/sampling.rb +1 -1
  39. data/lib/action_mcp/server/sampling_request.rb +115 -0
  40. data/lib/action_mcp/{transport → server}/tools.rb +1 -1
  41. data/lib/action_mcp/server/transport_handler.rb +41 -0
  42. data/lib/action_mcp/uri_ambiguity_checker.rb +6 -10
  43. data/lib/action_mcp/version.rb +1 -1
  44. data/lib/action_mcp.rb +2 -1
  45. metadata +31 -33
  46. data/lib/action_mcp/base_json_rpc_handler.rb +0 -97
  47. data/lib/action_mcp/client_json_rpc_handler.rb +0 -69
  48. data/lib/action_mcp/json_rpc_handler.rb +0 -229
  49. data/lib/action_mcp/sampling_request.rb +0 -113
  50. data/lib/action_mcp/server_json_rpc_handler.rb +0 -90
  51. data/lib/action_mcp/transport/transport_base.rb +0 -126
  52. data/lib/action_mcp/transport_handler.rb +0 -39
@@ -4,34 +4,37 @@ require "faraday"
4
4
  require "uri"
5
5
 
6
6
  module ActionMCP
7
- module Transport
8
- class SSEClient < TransportBase
9
- TIMEOUT = 10 # Increased from 1 second
10
- ENDPOINT_TIMEOUT = 5 # Seconds
11
-
7
+ module Client
8
+ # MCP client using Server-Sent Events (SSE) transport
9
+ class SSEClient < Base
12
10
  # Define a custom error class for connection issues
13
11
  class ConnectionError < StandardError; end
14
12
 
15
- def initialize(url, **options)
16
- super(**options)
13
+ SSE_TIMEOUT = 10
14
+ ENDPOINT_TIMEOUT = 5 # Seconds
15
+
16
+ attr_reader :base_url, :sse_path, :post_url, :session
17
+
18
+ def initialize(url, connect: true, logger: ActionMCP.logger, **_options)
19
+ super(logger: logger)
20
+ @type = :sse
17
21
  setup_connection(url)
18
- @buffer = ""
22
+ @buffer = +""
19
23
  @stop_requested = false
20
24
  @endpoint_received = false
21
25
  @endpoint_mutex = Mutex.new
22
26
  @endpoint_condition = ConditionVariable.new
23
-
24
- # Add connection state management
25
27
  @connection_mutex = Mutex.new
26
28
  @connection_condition = ConditionVariable.new
27
- @connection_error = nil
28
- @connected = false
29
+
30
+ self.connect if connect
29
31
  end
30
32
 
31
- def start(initialize_request_id)
32
- log_info("Connecting to #{@base_url}#{@sse_path}...")
33
+ protected
34
+
35
+ def start_transport
36
+ log_debug("Connecting to #{@base_url}#{@sse_path}...")
33
37
  @stop_requested = false
34
- @initialize_request_id = initialize_request_id
35
38
 
36
39
  # Reset connection state before starting
37
40
  @connection_mutex.synchronize do
@@ -42,10 +45,49 @@ module ActionMCP
42
45
  # Start connection thread
43
46
  @sse_thread = Thread.new { listen_sse }
44
47
 
45
- # Wait for endpoint instead of connection completion
48
+ # Wait for endpoint
46
49
  wait_for_endpoint
47
50
  end
48
51
 
52
+ def stop_transport
53
+ log_debug("Stopping SSE connection...")
54
+ @stop_requested = true
55
+ cleanup_sse_thread
56
+ end
57
+
58
+ def send_message(json_rpc)
59
+ response = @conn.post(post_url,
60
+ json_rpc,
61
+ { "Content-Type" => "application/json" })
62
+ response.success?
63
+ end
64
+
65
+ def ready?
66
+ endpoint_ready?
67
+ end
68
+
69
+ private
70
+
71
+ def setup_connection(url)
72
+ uri = URI.parse(url)
73
+ @base_url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
74
+ @sse_path = uri.path
75
+
76
+ @conn = Faraday.new(url: @base_url) do |f|
77
+ f.headers["User-Agent"] = user_agent
78
+ f.options.timeout = nil # No read timeout
79
+ f.options.open_timeout = SSE_TIMEOUT # Connection timeout
80
+
81
+ # Use Net::HTTP adapter explicitly as it works well with streaming
82
+ f.adapter :net_http do |http|
83
+ http.read_timeout = nil # No read timeout at adapter level too
84
+ http.open_timeout = SSE_TIMEOUT # Connection timeout
85
+ end
86
+ end
87
+
88
+ @post_url = nil
89
+ end
90
+
49
91
  def wait_for_endpoint
50
92
  success = false
51
93
  error = nil
@@ -80,62 +122,12 @@ module ActionMCP
80
122
  success
81
123
  end
82
124
 
83
- def send_message(json_rpc)
84
- # Wait for endpoint if not yet received
85
- unless endpoint_ready?
86
- log_info("Waiting for endpoint before sending message...")
87
- wait_for_endpoint
88
- end
89
-
90
- validate_post_endpoint
91
- log_debug("\e[34m--> #{json_rpc}\e[0m")
92
- send_http_request(json_rpc)
93
- end
94
-
95
- def stop
96
- log_info("Stopping SSE connection...")
97
- @stop_requested = true
98
- cleanup_sse_thread
99
- end
100
-
101
- def ready?
102
- endpoint_ready?
103
- end
104
-
105
- private
106
-
107
- def setup_connection(url)
108
- uri = URI.parse(url)
109
- @base_url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
110
- @sse_path = uri.path
111
-
112
- @conn = Faraday.new(url: @base_url) do |f|
113
- f.headers["User-Agent"] = user_agent
114
-
115
- f.options.timeout = nil # No read timeout
116
- f.options.open_timeout = 10 # Connection timeout
117
-
118
- # Use Net::HTTP adapter explicitly as it works well with streaming
119
- f.adapter :net_http do |http|
120
- # Configure the adapter directly
121
- http.read_timeout = nil # No read timeout at adapter level too
122
- http.open_timeout = 10 # Connection timeout
123
- end
124
-
125
- # Add logger middleware
126
- # f.response :logger, @logger, headers: true, bodies: true
127
- end
128
-
129
- @post_url = nil
130
- end
131
-
132
125
  def endpoint_ready?
133
126
  @endpoint_mutex.synchronize { @endpoint_received }
134
127
  end
135
128
 
136
- # The listen_sse method should NOT mark connection as successful at the end
137
129
  def listen_sse
138
- log_info("Starting SSE listener...")
130
+ log_debug("Starting SSE listener...")
139
131
 
140
132
  begin
141
133
  @conn.get(@sse_path) do |req|
@@ -151,8 +143,6 @@ module ActionMCP
151
143
  # as the SSE connection stays open
152
144
  rescue Faraday::ConnectionFailed => e
153
145
  handle_connection_error(format_connection_error(e))
154
- rescue StandardError => e
155
- handle_connection_error("Unexpected error: #{e.message}")
156
146
  end
157
147
  end
158
148
 
@@ -164,26 +154,18 @@ module ActionMCP
164
154
  end
165
155
  end
166
156
 
167
- def handle_connection_error(message)
168
- log_error("SSE connection failed: #{message}")
169
-
170
- # Set error and notify waiting threads
157
+ def connection_error=(message)
171
158
  @connection_mutex.synchronize do
172
159
  @connection_error = message
173
160
  @connection_condition.broadcast
174
161
  end
175
-
176
- @on_error&.call(StandardError.new(message))
177
162
  end
178
163
 
179
- # Send the initialized notification to the server
180
- def send_initialized_notification
181
- notification = JsonRpc::Notification.new(
182
- method: "notifications/initialized"
183
- )
184
-
185
- logger.info("Sent initialized notification to server")
186
- send_message(notification.to_json)
164
+ def handle_connection_error(message)
165
+ log_error("SSE connection failed: #{message}")
166
+ self.connection_error = message
167
+ @connected = false
168
+ @error_callback&.call(StandardError.new(message))
187
169
  end
188
170
 
189
171
  def handle_sse_data(chunk, _overall_bytes)
@@ -195,9 +177,9 @@ module ActionMCP
195
177
  @buffer << chunk
196
178
  # If the buffer does not contain a newline but appears to be a complete JSON object,
197
179
  # flush it as a complete event.
198
- if @buffer.strip.start_with?("{") && @buffer.strip.end_with?("}")
180
+ if @buffer.strip.match?(/^\{.*\}$/)
199
181
  (@current_event ||= []) << @buffer.strip
200
- @buffer = ""
182
+ @buffer = +""
201
183
  return handle_complete_event
202
184
  end
203
185
  process_buffer while @buffer.include?("\n")
@@ -227,7 +209,7 @@ module ActionMCP
227
209
  end
228
210
 
229
211
  def parse_event(lines)
230
- event_data = { type: "message", data: "" }
212
+ event_data = { type: "message", data: +"" }
231
213
  has_data_prefix = false
232
214
 
233
215
  lines.each do |line|
@@ -248,15 +230,13 @@ module ActionMCP
248
230
  case event_data[:type]
249
231
  when "endpoint" then set_post_endpoint(event_data[:data])
250
232
  when "message" then handle_raw_message(event_data[:data])
251
- when "ping" then log_debug("Received ping")
252
233
  else log_error("Unknown event type: #{event_data[:type]}")
253
234
  end
254
235
  end
255
236
 
256
- # Modify set_post_endpoint to mark connection as ready
257
237
  def set_post_endpoint(endpoint_path)
258
238
  @post_url = build_post_url(endpoint_path)
259
- log_info("Received POST endpoint: #{@post_url}")
239
+ log_debug("Received POST endpoint: #{post_url}")
260
240
 
261
241
  # Signal that we have received the endpoint
262
242
  @endpoint_mutex.synchronize do
@@ -274,31 +254,10 @@ module ActionMCP
274
254
  "#{@base_url}#{endpoint_path}"
275
255
  end
276
256
 
277
- def validate_post_endpoint
278
- raise "MCP endpoint not set (no 'endpoint' event received)" unless @post_url
279
- end
280
-
281
- def send_http_request(json_rpc)
282
- response = @conn.post(@post_url,
283
- json_rpc,
284
- { "Content-Type" => "application/json" })
285
- handle_http_response(response)
286
- end
287
-
288
- def handle_http_response(response)
289
- return if response.success?
290
-
291
- log_error("HTTP POST failed: #{response.status} - #{response.body}")
292
- end
293
-
294
257
  def cleanup_sse_thread
295
258
  return unless @sse_thread
296
259
 
297
- @sse_thread.join(TIMEOUT) || @sse_thread.kill
298
- end
299
-
300
- def user_agent
301
- "ActionMCP-sse-client"
260
+ @sse_thread.join(SSE_TIMEOUT) || @sse_thread.kill
302
261
  end
303
262
  end
304
263
  end
@@ -1,65 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
- module ActionMCP
5
- module Transport
6
- class StdioClient < TransportBase
7
- attr_reader :received_server_message
8
4
 
9
- def initialize(command, **options)
10
- super(**options)
11
- @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(command)
5
+ module ActionMCP
6
+ module Client
7
+ # MCP client using Standard I/O (STDIO) transport, Not tested for now
8
+ class StdioClient < Base
9
+ def initialize(command, logger: ActionMCP.logger, **_options)
10
+ super(logger: logger)
11
+ @type = :stdio
12
+ @command = command
12
13
  @threads_started = false
13
14
  @received_server_message = false
14
15
  @capabilities_sent = false
15
16
  end
16
17
 
17
- def start
18
+ protected
19
+
20
+ def start_transport
21
+ setup_stdio_process
18
22
  start_output_threads
19
23
 
20
24
  # Just log that connection is established but don't send capabilities yet
21
25
  if @threads_started && @wait_thr.alive?
22
- log_info("STDIO connection established")
26
+ log_debug("STDIO connection established")
27
+ true
23
28
  else
24
- log_error("Failed to start STDIO threads or process is not alive")
29
+ log_debug("Failed to start STDIO threads or process is not alive")
30
+ false
25
31
  end
26
32
  end
27
33
 
34
+ def stop_transport
35
+ cleanup_resources
36
+ end
37
+
28
38
  def send_message(json)
29
39
  log_debug("\e[34m--> #{json}\e[0m")
30
40
  @stdin.puts("#{json}\n\n")
31
41
  end
32
42
 
33
- def stop
34
- cleanup_resources
35
- end
36
-
37
43
  def ready?
38
- true
39
- end
40
-
41
- # Check if we've received any message from the server
42
- def received_server_message?
43
44
  @received_server_message
44
45
  end
45
46
 
46
- # Mark the client as ready and send initial capabilities if not already sent
47
- def mark_ready_and_send_capabilities
48
- return if @received_server_message
49
-
50
- @received_server_message = true
51
- log_info("Received first server message")
52
-
53
- # Send initial capabilities if not already sent
54
- return if @capabilities_sent
47
+ private
55
48
 
56
- log_info("Server is ready, sending initial capabilities...")
57
- send_initial_capabilities
58
- @capabilities_sent = true
49
+ def setup_stdio_process
50
+ @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(@command)
59
51
  end
60
52
 
61
- private
62
-
63
53
  def start_output_threads
64
54
  @stdout_thread = Thread.new do
65
55
  @stdout.each_line do |line|
@@ -75,7 +65,6 @@ module ActionMCP
75
65
  @stderr_thread = Thread.new do
76
66
  @stderr.each_line do |line|
77
67
  line = line.chomp
78
- log_info(line)
79
68
 
80
69
  # Check stderr for server messages
81
70
  mark_ready_and_send_capabilities if line.include?("MCP Server") || line.include?("running on stdio")
@@ -85,6 +74,21 @@ module ActionMCP
85
74
  @threads_started = true
86
75
  end
87
76
 
77
+ # Mark the client as ready and send initial capabilities if not already sent
78
+ def mark_ready_and_send_capabilities
79
+ return if @received_server_message
80
+
81
+ @received_server_message = true
82
+ log_debug("Received first server message")
83
+
84
+ # Send initial capabilities if not already sent
85
+ return if @capabilities_sent
86
+
87
+ log_debug("Server is ready, sending initial capabilities...")
88
+ send_initial_capabilities
89
+ @capabilities_sent = true
90
+ end
91
+
88
92
  def cleanup_resources
89
93
  @stdin.close
90
94
  wait_for_server_exit
@@ -106,10 +110,6 @@ module ActionMCP
106
110
  @stdout_thread&.kill
107
111
  @stderr_thread&.kill
108
112
  end
109
-
110
- def user_agent
111
- "ActionMCP-stdio-client"
112
- end
113
113
  end
114
114
  end
115
115
  end
@@ -0,0 +1,194 @@
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 && 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
108
+
109
+ # Initialize a new Tool instance
110
+ #
111
+ # @param data [Hash] Tool definition hash containing name, description, and inputSchema
112
+ def initialize(data)
113
+ @name = data["name"]
114
+ @description = data["description"]
115
+ @input_schema = data["inputSchema"] || {}
116
+ end
117
+
118
+ # Get all required properties for this tool
119
+ #
120
+ # @return [Array<String>] Array of required property names
121
+ def required_properties
122
+ @input_schema["required"] || []
123
+ end
124
+
125
+ # Get all properties for this tool
126
+ #
127
+ # @return [Hash] Hash of property definitions
128
+ def properties
129
+ @input_schema.dig("properties") || {}
130
+ end
131
+
132
+ # Check if the tool requires a specific property
133
+ #
134
+ # @param name [String] Name of the property to check
135
+ # @return [Boolean] true if the property is required
136
+ def requires?(name)
137
+ required_properties.include?(name)
138
+ end
139
+
140
+ # Check if the tool has a specific property
141
+ #
142
+ # @param name [String] Name of the property to check
143
+ # @return [Boolean] true if the property exists
144
+ def has_property?(name)
145
+ properties.key?(name)
146
+ end
147
+
148
+ # Get property details by name
149
+ #
150
+ # @param name [String] Name of the property
151
+ # @return [Hash, nil] Property details or nil if not found
152
+ def property(name)
153
+ properties[name]
154
+ end
155
+
156
+ # Generate a hash representation of the tool (default format)
157
+ #
158
+ # @return [Hash] Hash containing tool details
159
+ def to_h
160
+ {
161
+ "name" => @name,
162
+ "description" => @description,
163
+ "inputSchema" => @input_schema
164
+ }
165
+ end
166
+
167
+ # Generate a hash representation of the tool in Claude format
168
+ #
169
+ # @return [Hash] Hash containing tool details formatted for Claude
170
+ def to_claude_h
171
+ {
172
+ "name" => @name,
173
+ "description" => @description,
174
+ "input_schema" => @input_schema.transform_keys { |k| k == "inputSchema" ? "input_schema" : k }
175
+ }
176
+ end
177
+
178
+ # Generate a hash representation of the tool in OpenAI format
179
+ #
180
+ # @return [Hash] Hash containing tool details formatted for OpenAI
181
+ def to_openai_h
182
+ {
183
+ "type" => "function",
184
+ "function" => {
185
+ "name" => @name,
186
+ "description" => @description,
187
+ "parameters" => @input_schema
188
+ }
189
+ }
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Client
5
+ module Tools
6
+ # List all available tools from the server
7
+ # @return [String] Request ID for tracking the request
8
+ def list_tools
9
+ request_id = SecureRandom.uuid_v7
10
+
11
+ # Send request
12
+ send_jsonrpc_request("tools/list", id: request_id)
13
+
14
+ # Return request ID for timeout tracking
15
+ request_id
16
+ end
17
+
18
+ # Call a specific tool on the server
19
+ # @param name [String] Name of the tool to call
20
+ # @param arguments [Hash] Arguments to pass to the tool
21
+ # @return [String] Request ID for tracking the request
22
+ def call_tool(name, arguments)
23
+ request_id = SecureRandom.uuid_v7
24
+
25
+ # Send request
26
+ send_jsonrpc_request("tools/call",
27
+ params: {
28
+ name: name,
29
+ arguments: arguments
30
+ },
31
+ id: request_id
32
+ )
33
+
34
+ # Return request ID for tracking the request
35
+ request_id
36
+ end
37
+ end
38
+ end
39
+ end