ruby_llm-mcp 0.5.1 → 0.6.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.
@@ -10,15 +10,16 @@ module RubyLLM
10
10
  module MCP
11
11
  module Transports
12
12
  class SSE
13
- include Timeout
13
+ include Support::Timeout
14
14
 
15
15
  attr_reader :headers, :id, :coordinator
16
16
 
17
- def initialize(url:, coordinator:, request_timeout:, headers: {})
17
+ def initialize(url:, coordinator:, request_timeout:, version: :http2, headers: {})
18
18
  @event_url = url
19
19
  @messages_url = nil
20
20
  @coordinator = coordinator
21
21
  @request_timeout = request_timeout
22
+ @version = version
22
23
 
23
24
  uri = URI.parse(url)
24
25
  @root_url = "#{uri.scheme}://#{uri.host}"
@@ -44,7 +45,7 @@ module RubyLLM
44
45
  RubyLLM::MCP.logger.info "Initializing SSE transport to #{@event_url} with client ID #{@client_id}"
45
46
  end
46
47
 
47
- def request(body, add_id: true, wait_for_response: true) # rubocop:disable Metrics/MethodLength
48
+ def request(body, add_id: true, wait_for_response: true)
48
49
  if add_id
49
50
  @id_mutex.synchronize { @id_counter += 1 }
50
51
  request_id = @id_counter
@@ -59,34 +60,20 @@ module RubyLLM
59
60
  end
60
61
 
61
62
  begin
62
- http_client = HTTPClient.connection.with(timeout: { request_timeout: @request_timeout / 1000 },
63
- headers: @headers)
64
- response = http_client.post(@messages_url, body: JSON.generate(body))
65
-
66
- unless response.status == 200
67
- @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
68
- RubyLLM::MCP.logger.error "SSE request failed: #{response.status} - #{response.body}"
69
- raise Errors::TransportError.new(
70
- message: "Failed to request #{@messages_url}: #{response.status} - #{response.body}",
71
- code: response.status
72
- )
73
- end
74
- rescue StandardError => e
63
+ send_request(body, request_id)
64
+ rescue Errors::TransportError, Errors::TimeoutError => e
75
65
  @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
76
- RubyLLM::MCP.logger.error "SSE request error (ID: #{request_id}): #{e.message}"
77
- raise RubyLLM::MCP::Errors::TransportError.new(
78
- message: e.message,
79
- code: -1,
80
- error: e
81
- )
66
+ RubyLLM::MCP.logger.error "Request error (ID: #{request_id}): #{e.message}"
67
+ raise e
82
68
  end
69
+
83
70
  return unless wait_for_response
84
71
 
85
72
  begin
86
73
  with_timeout(@request_timeout / 1000, request_id: request_id) do
87
74
  response_queue.pop
88
75
  end
89
- rescue RubyLLM::MCP::Errors::TimeoutError => e
76
+ rescue Errors::TimeoutError => e
90
77
  @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
91
78
  RubyLLM::MCP.logger.error "SSE request timeout (ID: #{request_id}) after #{@request_timeout / 1000} seconds"
92
79
  raise e
@@ -117,6 +104,23 @@ module RubyLLM
117
104
 
118
105
  private
119
106
 
107
+ def send_request(body, request_id)
108
+ http_client = Support::HTTPClient.connection.with(timeout: { request_timeout: @request_timeout / 1000 },
109
+ headers: @headers)
110
+ response = http_client.post(@messages_url, body: JSON.generate(body))
111
+ handle_httpx_error_response!(response,
112
+ context: { location: "message endpoint request", request_id: request_id })
113
+
114
+ unless [200, 202].include?(response.status)
115
+ message = "Failed to have a successful request to #{@messages_url}: #{response.status} - #{response.body}"
116
+ RubyLLM::MCP.logger.error(message)
117
+ raise Errors::TransportError.new(
118
+ message: message,
119
+ code: response.status
120
+ )
121
+ end
122
+ end
123
+
120
124
  def start_sse_listener
121
125
  @connection_mutex.synchronize do
122
126
  return if sse_thread_running?
@@ -167,15 +171,36 @@ module RubyLLM
167
171
  sse_client = sse_client.with(
168
172
  headers: @headers
169
173
  )
174
+
175
+ if @version == :http1
176
+ sse_client = sse_client.with(
177
+ ssl: { alpn_protocols: ["http/1.1"] }
178
+ )
179
+ end
180
+
170
181
  response = sse_client.get(@event_url, stream: true)
182
+
183
+ event_buffer = []
171
184
  response.each_line do |event_line|
172
185
  unless @running
173
186
  response.body.close
174
187
  next
175
188
  end
176
189
 
177
- event = parse_event(event_line)
178
- process_event(event)
190
+ # Strip the line and check if it's empty (indicates end of event)
191
+ line = event_line.strip
192
+
193
+ if line.empty?
194
+ # End of event - process the accumulated buffer
195
+ if event_buffer.any?
196
+ event = parse_event(event_buffer.join("\n"))
197
+ process_event(event)
198
+ event_buffer.clear
199
+ end
200
+ else
201
+ # Accumulate the line for the current event
202
+ event_buffer << line
203
+ end
179
204
  end
180
205
  end
181
206
 
@@ -187,6 +212,25 @@ module RubyLLM
187
212
  sleep 1
188
213
  end
189
214
 
215
+ def handle_httpx_error_response!(response, context:)
216
+ return false unless response.is_a?(HTTPX::ErrorResponse)
217
+
218
+ error = response.error
219
+
220
+ if error.is_a?(HTTPX::ReadTimeoutError)
221
+ raise Errors::TimeoutError.new(
222
+ message: "Request timed out after #{@request_timeout / 1000} seconds"
223
+ )
224
+ end
225
+
226
+ error_message = response.error&.message || "Request failed"
227
+
228
+ raise Errors::TransportError.new(
229
+ code: nil,
230
+ message: "Request Error #{context}: #{error_message}"
231
+ )
232
+ end
233
+
190
234
  def process_event(raw_event)
191
235
  # Return if we believe that are getting a partial event
192
236
  return if raw_event[:data].nil?
@@ -9,11 +9,11 @@ module RubyLLM
9
9
  module MCP
10
10
  module Transports
11
11
  class Stdio
12
- include Timeout
12
+ include Support::Timeout
13
13
 
14
14
  attr_reader :command, :stdin, :stdout, :stderr, :id, :coordinator
15
15
 
16
- def initialize(command:, request_timeout:, coordinator:, args: [], env: {})
16
+ def initialize(command:, coordinator:, request_timeout:, args: [], env: {})
17
17
  @request_timeout = request_timeout
18
18
  @command = command
19
19
  @coordinator = coordinator
@@ -27,6 +27,21 @@ module RubyLLM
27
27
  end
28
28
  end
29
29
 
30
+ class OAuthOptions
31
+ attr_reader :issuer, :client_id, :client_secret, :scope
32
+
33
+ def initialize(issuer:, client_id:, client_secret:, scopes:)
34
+ @issuer = issuer
35
+ @client_id = client_id
36
+ @client_secret = client_secret
37
+ @scope = scopes
38
+ end
39
+
40
+ def enabled?
41
+ @issuer && @client_id && @client_secret && @scope
42
+ end
43
+ end
44
+
30
45
  # Options for starting SSE connections
31
46
  class StartSSEOptions
32
47
  attr_reader :resumption_token, :on_resumption_token, :replay_message_id
@@ -40,7 +55,7 @@ module RubyLLM
40
55
 
41
56
  # Main StreamableHTTP transport class
42
57
  class StreamableHTTP
43
- include Timeout
58
+ include Support::Timeout
44
59
 
45
60
  attr_reader :session_id, :protocol_version, :coordinator
46
61
 
@@ -49,6 +64,10 @@ module RubyLLM
49
64
  request_timeout:,
50
65
  coordinator:,
51
66
  headers: {},
67
+ reconnection: {},
68
+ version: :http2,
69
+ oauth: nil,
70
+ rate_limit: nil,
52
71
  reconnection_options: nil,
53
72
  session_id: nil
54
73
  )
@@ -57,11 +76,19 @@ module RubyLLM
57
76
  @request_timeout = request_timeout
58
77
  @headers = headers || {}
59
78
  @session_id = session_id
79
+
80
+ @version = version
60
81
  @reconnection_options = reconnection_options || ReconnectionOptions.new
61
82
  @protocol_version = nil
83
+ @session_id = session_id
84
+
62
85
  @resource_metadata_url = nil
63
86
  @client_id = SecureRandom.uuid
64
87
 
88
+ @reconnection_options = ReconnectionOptions.new(**reconnection)
89
+ @oauth_options = OAuthOptions.new(**oauth) unless oauth.nil?
90
+ @rate_limiter = Support::RateLimiter.new(**rate_limit) if rate_limit
91
+
65
92
  @id_counter = 0
66
93
  @id_mutex = Mutex.new
67
94
  @pending_requests = {}
@@ -79,6 +106,11 @@ module RubyLLM
79
106
  end
80
107
 
81
108
  def request(body, add_id: true, wait_for_response: true)
109
+ if @rate_limiter&.exceeded?
110
+ sleep(1) while @rate_limiter&.exceeded?
111
+ end
112
+ @rate_limiter&.add
113
+
82
114
  # Generate a unique request ID for requests
83
115
  if add_id && body.is_a?(Hash) && !body.key?("id")
84
116
  @id_mutex.synchronize { @id_counter += 1 }
@@ -202,7 +234,7 @@ module RubyLLM
202
234
  end
203
235
 
204
236
  def create_connection
205
- client = HTTPClient.connection.with(
237
+ client = Support::HTTPClient.connection.with(
206
238
  timeout: {
207
239
  connect_timeout: 10,
208
240
  read_timeout: @request_timeout / 1000,
@@ -210,6 +242,18 @@ module RubyLLM
210
242
  operation_timeout: @request_timeout / 1000
211
243
  }
212
244
  )
245
+
246
+ if @oauth_options&.enabled?
247
+ client = client.plugin(:oauth).oauth_auth(
248
+ issuer: @oauth_options.issuer,
249
+ client_id: @oauth_options.client_id,
250
+ client_secret: @oauth_options.client_secret,
251
+ scope: @oauth_options.scope
252
+ )
253
+
254
+ client.with_access_token
255
+ end
256
+
213
257
  register_client(client)
214
258
  end
215
259
 
@@ -219,6 +263,7 @@ module RubyLLM
219
263
  headers["mcp-session-id"] = @session_id if @session_id
220
264
  headers["mcp-protocol-version"] = @protocol_version if @protocol_version
221
265
  headers["X-CLIENT-ID"] = @client_id
266
+ headers["Origin"] = @uri.to_s
222
267
 
223
268
  headers
224
269
  end
@@ -259,7 +304,8 @@ module RubyLLM
259
304
  def create_connection_with_streaming_callbacks(request_id)
260
305
  buffer = +""
261
306
 
262
- client = HTTPClient.connection.plugin(:callbacks).on_response_body_chunk do |request, _response, chunk|
307
+ client = Support::HTTPClient.connection.plugin(:callbacks)
308
+ .on_response_body_chunk do |request, _response, chunk|
263
309
  next unless @running && !@abort_controller
264
310
 
265
311
  RubyLLM::MCP.logger.debug "Received chunk: #{chunk.bytesize} bytes for #{request.uri}"
@@ -274,6 +320,17 @@ module RubyLLM
274
320
  operation_timeout: @request_timeout / 1000
275
321
  }
276
322
  )
323
+
324
+ if @oauth_options&.enabled?
325
+ client = client.plugin(:oauth).oauth_auth(
326
+ issuer: @oauth_options.issuer,
327
+ client_id: @oauth_options.client_id,
328
+ client_secret: @oauth_options.client_secret,
329
+ scope: @oauth_options.scope
330
+ )
331
+
332
+ client.with_access_token
333
+ end
277
334
  register_client(client)
278
335
  end
279
336
 
@@ -422,8 +479,8 @@ module RubyLLM
422
479
  end
423
480
 
424
481
  # Set up SSE streaming connection with callbacks
425
- connection = create_connection_with_sse_callbacks(options)
426
- response = connection.get(@url, headers: headers)
482
+ connection = create_connection_with_sse_callbacks(options, headers)
483
+ response = connection.get(@url)
427
484
 
428
485
  # Handle HTTPX error responses first
429
486
  error_result = handle_httpx_error_response!(response, context: { location: "SSE connection" },
@@ -463,12 +520,32 @@ module RubyLLM
463
520
  end
464
521
  end
465
522
 
466
- def create_connection_with_sse_callbacks(options)
467
- buffer = +""
523
+ def create_connection_with_sse_callbacks(options, headers)
524
+ client = HTTPX.plugin(:callbacks)
525
+ client = add_on_response_body_chunk_callback(client, options)
526
+
527
+ client = client.with(
528
+ timeout: {
529
+ connect_timeout: 10,
530
+ read_timeout: @request_timeout / 1000,
531
+ write_timeout: @request_timeout / 1000,
532
+ operation_timeout: @request_timeout / 1000
533
+ },
534
+ headers: headers
535
+ )
468
536
 
469
- client = HTTPX
470
- .plugin(:callbacks)
471
- .on_response_body_chunk do |request, response, chunk|
537
+ if @version == :http1
538
+ client = client.with(
539
+ ssl: { alpn_protocols: ["http/1.1"] }
540
+ )
541
+ end
542
+
543
+ register_client(client)
544
+ end
545
+
546
+ def add_on_response_body_chunk_callback(client, options)
547
+ buffer = +""
548
+ client.on_response_body_chunk do |request, response, chunk|
472
549
  # Only process chunks for text/event-stream and if still running
473
550
  next unless @running && !@abort_controller
474
551
 
@@ -495,15 +572,6 @@ module RubyLLM
495
572
  end
496
573
  end
497
574
  end
498
- .with(
499
- timeout: {
500
- connect_timeout: 10,
501
- read_timeout: @request_timeout / 1000,
502
- write_timeout: @request_timeout / 1000,
503
- operation_timeout: @request_timeout / 1000
504
- }
505
- )
506
- register_client(client)
507
575
  end
508
576
 
509
577
  def calculate_reconnection_delay(attempt)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+
5
+ module RubyLLM
6
+ module MCP
7
+ module Transports
8
+ module Support
9
+ class HTTPClient
10
+ CONNECTION_KEY = :ruby_llm_mcp_client_connection
11
+
12
+ def self.connection
13
+ Thread.current[CONNECTION_KEY] ||= build_connection
14
+ end
15
+
16
+ def self.build_connection
17
+ HTTPX.with(
18
+ pool_options: {
19
+ max_connections: RubyLLM::MCP.config.max_connections,
20
+ pool_timeout: RubyLLM::MCP.config.pool_timeout
21
+ }
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Transports
6
+ module Support
7
+ class RateLimit
8
+ def initialize(limit: 10, interval: 1000)
9
+ @limit = limit
10
+ @interval = interval
11
+ @timestamps = []
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def exceeded?
16
+ now = current_time
17
+
18
+ @mutex.synchronize do
19
+ purge_old(now)
20
+ @timestamps.size >= @limit
21
+ end
22
+ end
23
+
24
+ def add
25
+ now = current_time
26
+
27
+ @mutex.synchronize do
28
+ purge_old(now)
29
+ @timestamps << now
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def current_time
36
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ end
38
+
39
+ def purge_old(now)
40
+ cutoff = now - @interval
41
+ @timestamps.reject! { |t| t < cutoff }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Transports
6
+ module Support
7
+ module Timeout
8
+ def with_timeout(seconds, request_id: nil)
9
+ result = nil
10
+ exception = nil
11
+
12
+ worker = Thread.new do
13
+ result = yield
14
+ rescue StandardError => e
15
+ exception = e
16
+ end
17
+
18
+ if worker.join(seconds)
19
+ raise exception if exception
20
+
21
+ result
22
+ else
23
+ worker.kill # stop the thread (can still have some risk if shared resources)
24
+ raise RubyLLM::MCP::Errors::TimeoutError.new(
25
+ message: "Request timed out after #{seconds} seconds",
26
+ request_id: request_id
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- VERSION = "0.5.1"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/ruby_llm/mcp.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruby_llm"
4
+ require "json-schema"
4
5
  require "zeitwerk"
5
6
  require_relative "chat"
6
7
 
@@ -9,20 +10,21 @@ module RubyLLM
9
10
  module_function
10
11
 
11
12
  def clients(config = RubyLLM::MCP.config.mcp_configuration)
12
- @clients ||= {}
13
- config.map do |options|
14
- @clients[options[:name]] ||= Client.new(**options)
13
+ if @clients.nil?
14
+ @clients = {}
15
+ config.map do |options|
16
+ @clients[options[:name]] ||= Client.new(**options)
17
+ end
15
18
  end
19
+ @clients
16
20
  end
17
21
 
18
22
  def add_client(options)
19
- @clients ||= {}
20
- @clients[options[:name]] ||= Client.new(**options)
23
+ clients[options[:name]] ||= Client.new(**options)
21
24
  end
22
25
 
23
26
  def remove_client(name)
24
- @clients ||= {}
25
- client = @clients.delete(name)
27
+ client = clients.delete(name)
26
28
  client&.stop
27
29
  client
28
30
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Vice
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: httpx
@@ -24,6 +23,20 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '1.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json-schema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: ruby_llm
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -77,6 +90,7 @@ files:
77
90
  - lib/ruby_llm/mcp/configuration.rb
78
91
  - lib/ruby_llm/mcp/content.rb
79
92
  - lib/ruby_llm/mcp/coordinator.rb
93
+ - lib/ruby_llm/mcp/elicitation.rb
80
94
  - lib/ruby_llm/mcp/error.rb
81
95
  - lib/ruby_llm/mcp/errors.rb
82
96
  - lib/ruby_llm/mcp/logging.rb
@@ -87,6 +101,7 @@ files:
87
101
  - lib/ruby_llm/mcp/parameter.rb
88
102
  - lib/ruby_llm/mcp/progress.rb
89
103
  - lib/ruby_llm/mcp/prompt.rb
104
+ - lib/ruby_llm/mcp/protocol.rb
90
105
  - lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb
91
106
  - lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb
92
107
  - lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb
@@ -109,6 +124,7 @@ files:
109
124
  - lib/ruby_llm/mcp/resource.rb
110
125
  - lib/ruby_llm/mcp/resource_template.rb
111
126
  - lib/ruby_llm/mcp/response_handler.rb
127
+ - lib/ruby_llm/mcp/responses/elicitation.rb
112
128
  - lib/ruby_llm/mcp/responses/error.rb
113
129
  - lib/ruby_llm/mcp/responses/ping.rb
114
130
  - lib/ruby_llm/mcp/responses/roots_list.rb
@@ -119,11 +135,12 @@ files:
119
135
  - lib/ruby_llm/mcp/server_capabilities.rb
120
136
  - lib/ruby_llm/mcp/tool.rb
121
137
  - lib/ruby_llm/mcp/transport.rb
122
- - lib/ruby_llm/mcp/transports/http_client.rb
123
138
  - lib/ruby_llm/mcp/transports/sse.rb
124
139
  - lib/ruby_llm/mcp/transports/stdio.rb
125
140
  - lib/ruby_llm/mcp/transports/streamable_http.rb
126
- - lib/ruby_llm/mcp/transports/timeout.rb
141
+ - lib/ruby_llm/mcp/transports/support/http_client.rb
142
+ - lib/ruby_llm/mcp/transports/support/rate_limit.rb
143
+ - lib/ruby_llm/mcp/transports/support/timeout.rb
127
144
  - lib/ruby_llm/mcp/version.rb
128
145
  - lib/tasks/release.rake
129
146
  homepage: https://github.com/patvice/ruby_llm-mcp
@@ -137,7 +154,6 @@ metadata:
137
154
  bug_tracker_uri: https://github.com/patvice/ruby_llm-mcp/issues
138
155
  rubygems_mfa_required: 'true'
139
156
  allowed_push_host: https://rubygems.org
140
- post_install_message:
141
157
  rdoc_options: []
142
158
  require_paths:
143
159
  - lib
@@ -152,8 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
168
  - !ruby/object:Gem::Version
153
169
  version: '0'
154
170
  requirements: []
155
- rubygems_version: 3.5.11
156
- signing_key:
171
+ rubygems_version: 3.6.7
157
172
  specification_version: 4
158
173
  summary: A RubyLLM MCP Client
159
174
  test_files: []
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "httpx"
4
-
5
- module RubyLLM
6
- module MCP
7
- module Transports
8
- class HTTPClient
9
- CONNECTION_KEY = :ruby_llm_mcp_client_connection
10
-
11
- def self.connection
12
- Thread.current[CONNECTION_KEY] ||= build_connection
13
- end
14
-
15
- def self.build_connection
16
- HTTPX.with(
17
- pool_options: {
18
- max_connections: RubyLLM::MCP.config.max_connections,
19
- pool_timeout: RubyLLM::MCP.config.pool_timeout
20
- }
21
- )
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module MCP
5
- module Transports
6
- module Timeout
7
- def with_timeout(seconds, request_id: nil)
8
- result = nil
9
- exception = nil
10
-
11
- worker = Thread.new do
12
- result = yield
13
- rescue StandardError => e
14
- exception = e
15
- end
16
-
17
- if worker.join(seconds)
18
- raise exception if exception
19
-
20
- result
21
- else
22
- worker.kill # stop the thread (can still have some risk if shared resources)
23
- raise RubyLLM::MCP::Errors::TimeoutError.new(
24
- message: "Request timed out after #{seconds} seconds",
25
- request_id: request_id
26
- )
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end