ruby-mcp-client 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87cf7d5701adff89363dd653d263189bded230a32232bbbb496d41b4092afdec
4
- data.tar.gz: 9a3561d2f97f0ef518cf75dfce82a8521140c9cdc381aaef3f3b3658265a7298
3
+ metadata.gz: 79a86302428257274c4e620fae0b7ad45c6229cf381a956ef3b0a7e736f17f62
4
+ data.tar.gz: 80727b0aa48a992054dafd7c484eb611404b47d878ef2869e59e937a815718dc
5
5
  SHA512:
6
- metadata.gz: 361f1916a531f14ded3292e15ea4947b0e3715d71d2be71dd24ea198616a82c55e516379a59a455b6c14f9fb7b3b9a1d02bdd457fde0b975ece6320bbb6da23b
7
- data.tar.gz: b38270ec5a9ddce3a3689e6c8fb6e86efaa35453b6bcc8a590612d02fe1ef86040d62fd30fb95ff02640bb7b822bf8f473ae24fd571ad696069657f98a9d5c20
6
+ metadata.gz: 977acc1ae8ca48a17b7827f006ed82578eb7bc24bf9ba08b03e77493e3447febc82287242a410861fb675b1fb74b0b67525dc4f92c9f8b448cd1cad2e3afc875
7
+ data.tar.gz: f940ed03a52065a5d8687d1722a588693d69a9c69f820771b328fed3b1bb69068fc7110dbedc745ea4fc42d047afa8afe975b2ed821aa5d33ac8c4e25fac9da7
data/README.md CHANGED
@@ -100,9 +100,8 @@ result = client.send_rpc('another_method', params: { data: 123 }) # Uses first a
100
100
  client.send_notification('status_update', params: { status: 'ready' })
101
101
 
102
102
  # Check server connectivity
103
- client.ping # Basic connectivity check
104
- client.ping({ echo: "hello" }) # With optional parameters
105
- client.ping({}, server_index: 1) # Ping a specific server by index
103
+ client.ping # Basic connectivity check (zero-parameter heartbeat call)
104
+ client.ping(server_index: 1) # Ping a specific server by index
106
105
 
107
106
  # Clear cached tools to force fresh fetch on next list
108
107
  client.clear_cache
@@ -110,6 +109,61 @@ client.clear_cache
110
109
  client.cleanup
111
110
  ```
112
111
 
112
+ ### Server-Sent Events (SSE) Example
113
+
114
+ The SSE transport provides robust connection handling for remote MCP servers:
115
+
116
+ ```ruby
117
+ require 'mcp_client'
118
+ require 'logger'
119
+
120
+ # Optional logger for debugging
121
+ logger = Logger.new($stdout)
122
+ logger.level = Logger::INFO
123
+
124
+ # Create an MCP client that connects to a Playwright MCP server via SSE
125
+ # First run: npx @playwright/mcp@latest --port 8931
126
+ sse_client = MCPClient.create_client(
127
+ mcp_server_configs: [
128
+ MCPClient.sse_config(
129
+ base_url: 'http://localhost:8931/sse',
130
+ read_timeout: 30, # Timeout in seconds
131
+ )
132
+ ]
133
+ )
134
+
135
+ # List available tools
136
+ tools = sse_client.list_tools
137
+
138
+ # Launch a browser
139
+ result = sse_client.call_tool('browser_install', {})
140
+ result = sse_client.call_tool('browser_navigate', { url: 'about:blank' })
141
+ # No browser ID needed with these tool names
142
+
143
+ # Create a new page
144
+ page_result = sse_client.call_tool('browser_tab_new', {})
145
+ # No page ID needed with these tool names
146
+
147
+ # Navigate to a website
148
+ sse_client.call_tool('browser_navigate', { url: 'https://example.com' })
149
+
150
+ # Get page title
151
+ title_result = sse_client.call_tool('browser_snapshot', {})
152
+ puts "Page snapshot: #{title_result}"
153
+
154
+ # Take a screenshot
155
+ screenshot_result = sse_client.call_tool('browser_take_screenshot', {})
156
+
157
+ # Ping the server to verify connectivity
158
+ ping_result = sse_client.ping
159
+ puts "Ping successful: #{ping_result.inspect}"
160
+
161
+ # Clean up
162
+ sse_client.cleanup
163
+ ```
164
+
165
+ See `examples/mcp_sse_server_example.rb` for the full Playwright SSE example.
166
+
113
167
  ### Integration Examples
114
168
 
115
169
  The repository includes examples for integrating with popular AI APIs:
@@ -196,6 +250,7 @@ Complete examples can be found in the `examples/` directory:
196
250
  - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
197
251
  - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
198
252
  - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
253
+ - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
199
254
 
200
255
  ## MCP Server Compatibility
201
256
 
@@ -205,7 +260,20 @@ This client works with any MCP-compatible server, including:
205
260
  - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
206
261
  - Custom servers implementing the MCP protocol
207
262
 
208
- ### Server Implementation Features
263
+ ## Key Features
264
+
265
+ ### Client Features
266
+
267
+ - **Multiple transports** - Support for both stdio and SSE transports
268
+ - **Multiple servers** - Connect to multiple MCP servers simultaneously
269
+ - **Tool discovery** - Find tools by name or pattern
270
+ - **Atomic tool calls** - Simple API for invoking tools with parameters
271
+ - **Batch support** - Call multiple tools in a single operation
272
+ - **API conversions** - Built-in format conversion for OpenAI and Anthropic APIs
273
+ - **Thread safety** - Synchronized access for thread-safe operation
274
+ - **Server notifications** - Support for JSON-RPC notifications
275
+ - **Custom RPC methods** - Send any custom JSON-RPC method
276
+ - **Consistent error handling** - Rich error types for better exception handling
209
277
 
210
278
  ### Server-Sent Events (SSE) Implementation
211
279
 
@@ -226,7 +294,7 @@ The SSE client implementation provides these key features:
226
294
 
227
295
  ## Requirements
228
296
 
229
- - Ruby >= 2.7.0
297
+ - Ruby >= 3.2.0
230
298
  - No runtime dependencies
231
299
 
232
300
  ## Implementing an MCP Server
@@ -274,4 +342,4 @@ This gem is available as open source under the [MIT License](LICENSE).
274
342
  ## Contributing
275
343
 
276
344
  Bug reports and pull requests are welcome on GitHub at
277
- https://github.com/simonx1/ruby-mcp-client.
345
+ https://github.com/simonx1/ruby-mcp-client.
@@ -31,9 +31,9 @@ module MCPClient
31
31
  # Register default and user-defined notification handlers on each server
32
32
  @servers.each do |server|
33
33
  server.on_notification do |method, params|
34
- # Default handling: clear tool cache on tools list change
35
- clear_cache if method == 'notifications/tools/list_changed'
36
- # Invoke user listeners
34
+ # Default notification processing (e.g., cache invalidation, logging)
35
+ process_notification(server, method, params)
36
+ # Invoke user-defined listeners
37
37
  @notification_listeners.each { |cb| cb.call(server, method, params) }
38
38
  end
39
39
  end
@@ -163,17 +163,16 @@ module MCPClient
163
163
  end
164
164
  end
165
165
 
166
- # Ping the MCP server to check connectivity
167
- # @param params [Hash] optional parameters for the ping request
166
+ # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
168
167
  # @param server_index [Integer, nil] optional index of a specific server to ping, nil for first available
169
168
  # @return [Object] result from the ping request
170
169
  # @raise [MCPClient::Errors::ServerNotFound] if no server is available
171
- def ping(params = {}, server_index: nil)
170
+ def ping(server_index: nil)
172
171
  if server_index.nil?
173
172
  # Ping first available server
174
173
  raise MCPClient::Errors::ServerNotFound, 'No server available for ping' if @servers.empty?
175
174
 
176
- @servers.first.ping(params)
175
+ @servers.first.ping
177
176
  else
178
177
  # Ping specified server
179
178
  if server_index >= @servers.length
@@ -181,12 +180,78 @@ module MCPClient
181
180
  "Server at index #{server_index} not found"
182
181
  end
183
182
 
184
- @servers[server_index].ping(params)
183
+ @servers[server_index].ping
185
184
  end
186
185
  end
187
186
 
187
+ # Send a raw JSON-RPC request to a server
188
+ # @param method [String] JSON-RPC method name
189
+ # @param params [Hash] parameters for the request
190
+ # @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
191
+ # @return [Object] result from the JSON-RPC response
192
+ def send_rpc(method, params: {}, server: nil)
193
+ srv = select_server(server)
194
+ srv.rpc_request(method, params)
195
+ end
196
+
197
+ # Send a raw JSON-RPC notification to a server (no response expected)
198
+ # @param method [String] JSON-RPC method name
199
+ # @param params [Hash] parameters for the notification
200
+ # @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
201
+ # @return [void]
202
+ def send_notification(method, params: {}, server: nil)
203
+ srv = select_server(server)
204
+ srv.rpc_notify(method, params)
205
+ end
206
+
188
207
  private
189
208
 
209
+ # Process incoming JSON-RPC notifications with default handlers
210
+ # @param server [MCPClient::ServerBase] the server that emitted the notification
211
+ # @param method [String] JSON-RPC notification method
212
+ # @param params [Hash] parameters for the notification
213
+ # @return [void]
214
+ def process_notification(server, method, params)
215
+ case method
216
+ when 'notifications/tools/list_changed'
217
+ logger.warn("[#{server.class}] Tool list has changed, clearing tool cache")
218
+ clear_cache
219
+ when 'notifications/resources/updated'
220
+ logger.warn("[#{server.class}] Resource #{params['uri']} updated")
221
+ when 'notifications/prompts/list_changed'
222
+ logger.warn("[#{server.class}] Prompt list has changed")
223
+ when 'notifications/resources/list_changed'
224
+ logger.warn("[#{server.class}] Resource list has changed")
225
+ end
226
+ end
227
+
228
+ # Select a server based on index, type, or instance
229
+ # @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
230
+ # @return [MCPClient::ServerBase]
231
+ def select_server(server_arg)
232
+ case server_arg
233
+ when nil
234
+ raise MCPClient::Errors::ServerNotFound, 'No server available' if @servers.empty?
235
+
236
+ @servers.first
237
+ when Integer
238
+ @servers.fetch(server_arg) do
239
+ raise MCPClient::Errors::ServerNotFound, "Server at index #{server_arg} not found"
240
+ end
241
+ when String, Symbol
242
+ key = server_arg.to_s.downcase
243
+ srv = @servers.find { |s| s.class.name.split('::').last.downcase.end_with?(key) }
244
+ raise MCPClient::Errors::ServerNotFound, "Server of type #{server_arg} not found" unless srv
245
+
246
+ srv
247
+ else
248
+ raise ArgumentError, "Invalid server argument: #{server_arg.inspect}" unless @servers.include?(server_arg)
249
+
250
+ server_arg
251
+
252
+ end
253
+ end
254
+
190
255
  # Validate parameters against tool JSON schema (checks required properties)
191
256
  # @param tool [MCPClient::Tool] tool definition with schema
192
257
  # @param parameters [Hash] parameters to validate
@@ -45,11 +45,10 @@ module MCPClient
45
45
  raise NotImplementedError, 'Subclasses must implement rpc_notify'
46
46
  end
47
47
 
48
- # Ping the MCP server to check connectivity
49
- # @param params [Hash] optional parameters for the ping request
48
+ # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
50
49
  # @return [Object] result from the ping request
51
- def ping(params = {})
52
- rpc_request('ping', params)
50
+ def ping
51
+ rpc_request('ping')
53
52
  end
54
53
 
55
54
  # Register a callback to receive JSON-RPC notifications
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'uri'
4
- require 'net/http'
5
4
  require 'json'
6
- require 'openssl'
7
5
  require 'monitor'
8
6
  require 'logger'
7
+ require 'faraday'
8
+ require 'faraday/retry'
9
9
 
10
10
  module MCPClient
11
11
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
12
12
  # Useful for communicating with remote MCP servers over HTTP
13
13
  class ServerSSE < ServerBase
14
- attr_reader :base_url, :tools, :session_id, :http_client, :server_info, :capabilities
14
+ attr_reader :base_url, :tools, :server_info, :capabilities
15
15
 
16
16
  # @param base_url [String] The base URL of the MCP server
17
17
  # @param headers [Hash] Additional headers to include in requests
@@ -33,10 +33,12 @@ module MCPClient
33
33
  'Cache-Control' => 'no-cache',
34
34
  'Connection' => 'keep-alive'
35
35
  })
36
- @http_client = nil
36
+ # HTTP client is managed via Faraday
37
37
  @tools = nil
38
38
  @read_timeout = read_timeout
39
- @session_id = nil
39
+
40
+ # SSE-provided JSON-RPC endpoint path for POST requests
41
+ @rpc_endpoint = nil
40
42
  @tools_data = nil
41
43
  @request_id = 0
42
44
  @sse_results = {}
@@ -132,19 +134,7 @@ module MCPClient
132
134
  @mutex.synchronize do
133
135
  return true if @connection_established
134
136
 
135
- uri = URI.parse(@base_url)
136
- @http_client = Net::HTTP.new(uri.host, uri.port)
137
-
138
- if uri.scheme == 'https'
139
- @http_client.use_ssl = true
140
- @http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
141
- end
142
-
143
- @http_client.open_timeout = 10
144
- @http_client.read_timeout = @read_timeout
145
- @http_client.keep_alive_timeout = 60
146
-
147
- @http_client.start
137
+ # Start SSE listener using Faraday HTTP client
148
138
  start_sse_thread
149
139
 
150
140
  timeout = 10
@@ -179,7 +169,6 @@ module MCPClient
179
169
  end
180
170
 
181
171
  @tools = nil
182
- @session_id = nil
183
172
  @connection_established = false
184
173
  @sse_connected = false
185
174
  end
@@ -204,31 +193,31 @@ module MCPClient
204
193
  # @return [void]
205
194
  def rpc_notify(method, params = {})
206
195
  ensure_initialized
207
- url_base = @base_url.sub(%r{/sse/?$}, '')
208
- uri = URI.parse("#{url_base}/messages?sessionId=#{@session_id}")
209
- rpc_http = Net::HTTP.new(uri.host, uri.port)
210
- if uri.scheme == 'https'
211
- rpc_http.use_ssl = true
212
- rpc_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
196
+ uri = URI.parse(@base_url)
197
+ base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
198
+ rpc_ep = @mutex.synchronize { @rpc_endpoint }
199
+ @rpc_conn ||= Faraday.new(url: base) do |f|
200
+ f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
201
+ f.options.open_timeout = @read_timeout
202
+ f.options.timeout = @read_timeout
203
+ f.adapter Faraday.default_adapter
213
204
  end
214
- rpc_http.open_timeout = 10
215
- rpc_http.read_timeout = @read_timeout
216
- rpc_http.keep_alive_timeout = 60
217
- rpc_http.start do |http|
218
- http_req = Net::HTTP::Post.new(uri)
219
- http_req.content_type = 'application/json'
220
- http_req.body = { jsonrpc: '2.0', method: method, params: params }.to_json
221
- headers = @headers.dup
222
- headers.except('Accept', 'Cache-Control').each { |k, v| http_req[k] = v }
223
- response = http.request(http_req)
224
- unless response.is_a?(Net::HTTPSuccess)
225
- raise MCPClient::Errors::ServerError, "Notification failed: #{response.code} #{response.message}"
205
+ response = @rpc_conn.post(rpc_ep) do |req|
206
+ req.headers['Content-Type'] = 'application/json'
207
+ req.headers['Accept'] = 'application/json'
208
+ (@headers.dup.tap do |h|
209
+ h.delete('Accept')
210
+ h.delete('Cache-Control')
211
+ end).each do |k, v|
212
+ req.headers[k] = v
226
213
  end
214
+ req.body = { jsonrpc: '2.0', method: method, params: params }.to_json
215
+ end
216
+ unless response.success?
217
+ raise MCPClient::Errors::ServerError, "Notification failed: #{response.status} #{response.reason_phrase}"
227
218
  end
228
219
  rescue StandardError => e
229
220
  raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
230
- ensure
231
- rpc_http.finish if rpc_http&.started?
232
221
  end
233
222
 
234
223
  private
@@ -269,60 +258,31 @@ module MCPClient
269
258
  return if @sse_thread&.alive?
270
259
 
271
260
  @sse_thread = Thread.new do
272
- sse_http = nil
273
- begin
274
- uri = URI.parse(@base_url)
275
- sse_http = Net::HTTP.new(uri.host, uri.port)
276
-
277
- if uri.scheme == 'https'
278
- sse_http.use_ssl = true
279
- sse_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
280
- end
281
-
282
- sse_http.open_timeout = 10
283
- sse_http.read_timeout = @read_timeout
284
- sse_http.keep_alive_timeout = 60
285
-
286
- sse_http.start do |http|
287
- request = Net::HTTP::Get.new(uri)
288
- @headers.each { |k, v| request[k] = v }
289
-
290
- http.request(request) do |response|
291
- unless response.is_a?(Net::HTTPSuccess) && response['content-type']&.start_with?('text/event-stream')
292
- @mutex.synchronize do
293
- # Signal connection attempt completed (failed)
294
- @connection_established = false
295
- @connection_cv.broadcast
296
- end
297
- raise MCPClient::Errors::ServerError, 'Server response not OK or not text/event-stream'
298
- end
261
+ uri = URI.parse(@base_url)
262
+ sse_base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
263
+ sse_path = uri.request_uri
299
264
 
300
- @mutex.synchronize do
301
- # Signal connection established and SSE ready
302
- @sse_connected = true
303
- @connection_established = true
304
- @connection_cv.broadcast
305
- end
265
+ @sse_conn ||= Faraday.new(url: sse_base) do |f|
266
+ f.options.open_timeout = 10
267
+ f.options.timeout = nil
268
+ f.adapter Faraday.default_adapter
269
+ end
306
270
 
307
- response.read_body do |chunk|
308
- @logger.debug("SSE chunk received: #{chunk.inspect}")
309
- process_sse_chunk(chunk.dup)
310
- end
311
- end
312
- end
313
- rescue StandardError
314
- # On any SSE thread error, signal connection as established to unblock connect
315
- @mutex.synchronize do
316
- @connection_established = true
317
- @connection_cv.broadcast
318
- end
319
- nil
320
- ensure
321
- sse_http&.finish if sse_http&.started?
322
- @mutex.synchronize do
323
- @sse_connected = false
271
+ @sse_conn.get(sse_path) do |req|
272
+ @headers.each { |k, v| req.headers[k] = v }
273
+ req.options.on_data = proc do |chunk, _bytes|
274
+ @logger.debug("SSE chunk received: #{chunk.inspect}")
275
+ process_sse_chunk(chunk.dup)
324
276
  end
325
277
  end
278
+ rescue StandardError
279
+ # On any SSE thread error, signal connection established to unblock connect
280
+ @mutex.synchronize do
281
+ @connection_established = true
282
+ @connection_cv.broadcast
283
+ end
284
+ ensure
285
+ @mutex.synchronize { @sse_connected = false }
326
286
  end
327
287
  end
328
288
 
@@ -352,14 +312,11 @@ module MCPClient
352
312
 
353
313
  case event[:event]
354
314
  when 'endpoint'
355
- if event[:data].include?('sessionId=')
356
- session_id = event[:data].split('sessionId=').last
357
-
358
- @mutex.synchronize do
359
- @session_id = session_id
360
- @connection_established = true
361
- @connection_cv.broadcast
362
- end
315
+ ep = event[:data]
316
+ @mutex.synchronize do
317
+ @rpc_endpoint = ep
318
+ @connection_established = true
319
+ @connection_cv.broadcast
363
320
  end
364
321
  when 'message'
365
322
  begin
@@ -475,72 +432,56 @@ module MCPClient
475
432
  def send_jsonrpc_request(request)
476
433
  @logger.debug("Sending JSON-RPC request: #{request.to_json}")
477
434
  uri = URI.parse(@base_url)
478
- rpc_http = Net::HTTP.new(uri.host, uri.port)
479
-
480
- if uri.scheme == 'https'
481
- rpc_http.use_ssl = true
482
- rpc_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
435
+ base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
436
+ rpc_ep = @mutex.synchronize { @rpc_endpoint }
437
+
438
+ @rpc_conn ||= Faraday.new(url: base) do |f|
439
+ f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
440
+ f.options.open_timeout = @read_timeout
441
+ f.options.timeout = @read_timeout
442
+ f.adapter Faraday.default_adapter
483
443
  end
484
444
 
485
- rpc_http.open_timeout = 10
486
- rpc_http.read_timeout = @read_timeout
487
- rpc_http.keep_alive_timeout = 60
488
-
489
- begin
490
- rpc_http.start do |http|
491
- session_id = @mutex.synchronize { @session_id }
492
-
493
- url = if session_id
494
- "#{@base_url.sub(%r{/sse/?$}, '')}/messages?sessionId=#{session_id}"
495
- else
496
- "#{@base_url.sub(%r{/sse/?$}, '')}/messages"
497
- end
498
-
499
- uri = URI.parse(url)
500
- http_request = Net::HTTP::Post.new(uri)
501
- http_request.content_type = 'application/json'
502
- http_request.body = request.to_json
503
-
504
- headers = @mutex.synchronize { @headers.dup }
505
- headers.except('Accept', 'Cache-Control')
506
- .each { |k, v| http_request[k] = v }
445
+ response = @rpc_conn.post(rpc_ep) do |req|
446
+ req.headers['Content-Type'] = 'application/json'
447
+ req.headers['Accept'] = 'application/json'
448
+ (@headers.dup.tap do |h|
449
+ h.delete('Accept')
450
+ h.delete('Cache-Control')
451
+ end).each do |k, v|
452
+ req.headers[k] = v
453
+ end
454
+ req.body = request.to_json
455
+ end
456
+ @logger.debug("Received JSON-RPC response: #{response.status} #{response.body}")
507
457
 
508
- response = http.request(http_request)
509
- @logger.debug("Received JSON-RPC response: #{response.code} #{response.body}")
458
+ unless response.success?
459
+ raise MCPClient::Errors::ServerError, "Server returned error: #{response.status} #{response.reason_phrase}"
460
+ end
510
461
 
511
- unless response.is_a?(Net::HTTPSuccess)
512
- raise MCPClient::Errors::ServerError, "Server returned error: #{response.code} #{response.message}"
462
+ if @use_sse
463
+ # Wait for result via SSE channel
464
+ request_id = request[:id]
465
+ start_time = Time.now
466
+ timeout = @read_timeout || 10
467
+ loop do
468
+ result = nil
469
+ @mutex.synchronize do
470
+ result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
513
471
  end
472
+ return result if result
473
+ break if Time.now - start_time > timeout
514
474
 
515
- # If SSE transport is enabled, retrieve the result via the SSE channel
516
- if @use_sse
517
- request_id = request[:id]
518
- start_time = Time.now
519
- timeout = @read_timeout || 10
520
- result = nil
521
-
522
- loop do
523
- @mutex.synchronize do
524
- result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
525
- end
526
- break if result || (Time.now - start_time > timeout)
527
-
528
- sleep 0.1
529
- end
530
- return result if result
531
-
532
- raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
533
- end
534
- # Fallback: parse synchronous HTTP JSON response
535
- begin
536
- data = JSON.parse(response.body)
537
- return data['result']
538
- rescue JSON::ParserError => e
539
- raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
540
- end
475
+ sleep 0.1
476
+ end
477
+ raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
478
+ else
479
+ begin
480
+ data = JSON.parse(response.body)
481
+ data['result']
482
+ rescue JSON::ParserError => e
483
+ raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
541
484
  end
542
- ensure
543
- rpc_http.finish if rpc_http.started?
544
485
  end
545
486
  end
546
487
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.4.1'
5
+ VERSION = '0.5.0'
6
6
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Szymon Kurcab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-24 00:00:00.000000000 Z
11
+ date: 2025-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: rdoc
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -98,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
98
126
  requirements:
99
127
  - - ">="
100
128
  - !ruby/object:Gem::Version
101
- version: 2.7.0
129
+ version: 3.2.0
102
130
  required_rubygems_version: !ruby/object:Gem::Requirement
103
131
  requirements:
104
132
  - - ">="