ruby-mcp-client 0.4.0 → 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: fe40b5481b8c0b59201585bfe54fb912c71235d4f02ada0c8a32a908155c319d
4
- data.tar.gz: a378e9f8bd616e92739d371dfb458fc9385a6f2bbc62287cdf1e705742cfa6e0
3
+ metadata.gz: 79a86302428257274c4e620fae0b7ad45c6229cf381a956ef3b0a7e736f17f62
4
+ data.tar.gz: 80727b0aa48a992054dafd7c484eb611404b47d878ef2869e59e937a815718dc
5
5
  SHA512:
6
- metadata.gz: 19cb60e6ebf6693a3ee312e592b94e891b568bb67fc675b732d3c32c6452260486fe3f7197a4626e933096cbb52ae4afae8aaed763906fba3041adcadb7ee8ce
7
- data.tar.gz: c4c9d8a8b7aaf40d277bd20cb7aa50caf1ade083d6569e9825bb6dc53378d1359ef40e7dab84397416df950887b705abca7738179f639bd965ecb23e3d8186ea
6
+ metadata.gz: 977acc1ae8ca48a17b7827f006ed82578eb7bc24bf9ba08b03e77493e3447febc82287242a410861fb675b1fb74b0b67525dc4f92c9f8b448cd1cad2e3afc875
7
+ data.tar.gz: f940ed03a52065a5d8687d1722a588693d69a9c69f820771b328fed3b1bb69068fc7110dbedc745ea4fc42d047afa8afe975b2ed821aa5d33ac8c4e25fac9da7
data/README.md CHANGED
@@ -99,12 +99,71 @@ client.send_rpc('custom_method', params: { key: 'value' }, server: :sse) # Uses
99
99
  result = client.send_rpc('another_method', params: { data: 123 }) # Uses first available server
100
100
  client.send_notification('status_update', params: { status: 'ready' })
101
101
 
102
+ # Check server connectivity
103
+ client.ping # Basic connectivity check (zero-parameter heartbeat call)
104
+ client.ping(server_index: 1) # Ping a specific server by index
105
+
102
106
  # Clear cached tools to force fresh fetch on next list
103
107
  client.clear_cache
104
108
  # Clean up connections
105
109
  client.cleanup
106
110
  ```
107
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
+
108
167
  ### Integration Examples
109
168
 
110
169
  The repository includes examples for integrating with popular AI APIs:
@@ -191,6 +250,7 @@ Complete examples can be found in the `examples/` directory:
191
250
  - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
192
251
  - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
193
252
  - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
253
+ - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
194
254
 
195
255
  ## MCP Server Compatibility
196
256
 
@@ -200,7 +260,20 @@ This client works with any MCP-compatible server, including:
200
260
  - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
201
261
  - Custom servers implementing the MCP protocol
202
262
 
203
- ### 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
204
277
 
205
278
  ### Server-Sent Events (SSE) Implementation
206
279
 
@@ -215,10 +288,13 @@ The SSE client implementation provides these key features:
215
288
  - **Custom RPC methods**: Send any custom JSON-RPC method or notification through `send_rpc` and `send_notification`
216
289
  - **Configurable retries**: All RPC requests support configurable retries with exponential backoff
217
290
  - **Consistent logging**: Tagged, leveled logging across all components for better debugging
291
+ - **Graceful fallbacks**: Automatic fallback to synchronous HTTP when SSE connection fails
292
+ - **URL normalization**: Consistent URL handling that respects user-provided formats
293
+ - **Server connectivity check**: Built-in `ping` method to test server connectivity and health
218
294
 
219
295
  ## Requirements
220
296
 
221
- - Ruby >= 2.7.0
297
+ - Ruby >= 3.2.0
222
298
  - No runtime dependencies
223
299
 
224
300
  ## Implementing an MCP Server
@@ -266,4 +342,4 @@ This gem is available as open source under the [MIT License](LICENSE).
266
342
  ## Contributing
267
343
 
268
344
  Bug reports and pull requests are welcome on GitHub at
269
- 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,8 +163,95 @@ module MCPClient
163
163
  end
164
164
  end
165
165
 
166
+ # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
167
+ # @param server_index [Integer, nil] optional index of a specific server to ping, nil for first available
168
+ # @return [Object] result from the ping request
169
+ # @raise [MCPClient::Errors::ServerNotFound] if no server is available
170
+ def ping(server_index: nil)
171
+ if server_index.nil?
172
+ # Ping first available server
173
+ raise MCPClient::Errors::ServerNotFound, 'No server available for ping' if @servers.empty?
174
+
175
+ @servers.first.ping
176
+ else
177
+ # Ping specified server
178
+ if server_index >= @servers.length
179
+ raise MCPClient::Errors::ServerNotFound,
180
+ "Server at index #{server_index} not found"
181
+ end
182
+
183
+ @servers[server_index].ping
184
+ end
185
+ end
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
+
166
207
  private
167
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
+
168
255
  # Validate parameters against tool JSON schema (checks required properties)
169
256
  # @param tool [MCPClient::Tool] tool definition with schema
170
257
  # @param parameters [Hash] parameters to validate
@@ -45,6 +45,12 @@ module MCPClient
45
45
  raise NotImplementedError, 'Subclasses must implement rpc_notify'
46
46
  end
47
47
 
48
+ # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
49
+ # @return [Object] result from the ping request
50
+ def ping
51
+ rpc_request('ping')
52
+ end
53
+
48
54
  # Register a callback to receive JSON-RPC notifications
49
55
  # @yield [method, params] invoked when a notification is received
50
56
  # @return [void]
@@ -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
@@ -26,16 +26,19 @@ module MCPClient
26
26
  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
27
27
  @max_retries = retries
28
28
  @retry_backoff = retry_backoff
29
- @base_url = base_url.end_with?('/') ? base_url : "#{base_url}/"
29
+ # Normalize base_url: strip any trailing slash, use exactly as provided
30
+ @base_url = base_url.chomp('/')
30
31
  @headers = headers.merge({
31
32
  'Accept' => 'text/event-stream',
32
33
  'Cache-Control' => 'no-cache',
33
34
  'Connection' => 'keep-alive'
34
35
  })
35
- @http_client = nil
36
+ # HTTP client is managed via Faraday
36
37
  @tools = nil
37
38
  @read_timeout = read_timeout
38
- @session_id = nil
39
+
40
+ # SSE-provided JSON-RPC endpoint path for POST requests
41
+ @rpc_endpoint = nil
39
42
  @tools_data = nil
40
43
  @request_id = 0
41
44
  @sse_results = {}
@@ -45,6 +48,8 @@ module MCPClient
45
48
  @connection_established = false
46
49
  @connection_cv = @mutex.new_cond
47
50
  @initialized = false
51
+ # Whether to use SSE transport; may disable if handshake fails
52
+ @use_sse = true
48
53
  end
49
54
 
50
55
  # Stream tool call fallback for SSE transport (yields single result)
@@ -129,19 +134,7 @@ module MCPClient
129
134
  @mutex.synchronize do
130
135
  return true if @connection_established
131
136
 
132
- uri = URI.parse(@base_url)
133
- @http_client = Net::HTTP.new(uri.host, uri.port)
134
-
135
- if uri.scheme == 'https'
136
- @http_client.use_ssl = true
137
- @http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
138
- end
139
-
140
- @http_client.open_timeout = 10
141
- @http_client.read_timeout = @read_timeout
142
- @http_client.keep_alive_timeout = 60
143
-
144
- @http_client.start
137
+ # Start SSE listener using Faraday HTTP client
145
138
  start_sse_thread
146
139
 
147
140
  timeout = 10
@@ -176,7 +169,6 @@ module MCPClient
176
169
  end
177
170
 
178
171
  @tools = nil
179
- @session_id = nil
180
172
  @connection_established = false
181
173
  @sse_connected = false
182
174
  end
@@ -201,41 +193,42 @@ module MCPClient
201
193
  # @return [void]
202
194
  def rpc_notify(method, params = {})
203
195
  ensure_initialized
204
- url_base = @base_url.sub(%r{/sse/?$}, '')
205
- uri = URI.parse("#{url_base}/messages?sessionId=#{@session_id}")
206
- rpc_http = Net::HTTP.new(uri.host, uri.port)
207
- if uri.scheme == 'https'
208
- rpc_http.use_ssl = true
209
- 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
210
204
  end
211
- rpc_http.open_timeout = 10
212
- rpc_http.read_timeout = @read_timeout
213
- rpc_http.keep_alive_timeout = 60
214
- rpc_http.start do |http|
215
- http_req = Net::HTTP::Post.new(uri)
216
- http_req.content_type = 'application/json'
217
- http_req.body = { jsonrpc: '2.0', method: method, params: params }.to_json
218
- headers = @headers.dup
219
- headers.except('Accept', 'Cache-Control').each { |k, v| http_req[k] = v }
220
- response = http.request(http_req)
221
- unless response.is_a?(Net::HTTPSuccess)
222
- 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
223
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}"
224
218
  end
225
219
  rescue StandardError => e
226
220
  raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
227
- ensure
228
- rpc_http.finish if rpc_http&.started?
229
221
  end
230
222
 
231
223
  private
232
224
 
233
- # Ensure handshake initialization has been performed
225
+ # Ensure SSE initialization handshake has been performed
234
226
  def ensure_initialized
235
227
  return if @initialized
236
228
 
237
229
  connect
238
230
  perform_initialize
231
+
239
232
  @initialized = true
240
233
  end
241
234
 
@@ -265,51 +258,31 @@ module MCPClient
265
258
  return if @sse_thread&.alive?
266
259
 
267
260
  @sse_thread = Thread.new do
268
- sse_http = nil
269
- begin
270
- uri = URI.parse(@base_url)
271
- sse_http = Net::HTTP.new(uri.host, uri.port)
272
-
273
- if uri.scheme == 'https'
274
- sse_http.use_ssl = true
275
- sse_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
276
- end
277
-
278
- sse_http.open_timeout = 10
279
- sse_http.read_timeout = @read_timeout
280
- sse_http.keep_alive_timeout = 60
281
-
282
- sse_http.start do |http|
283
- request = Net::HTTP::Get.new(uri)
284
- @headers.each { |k, v| request[k] = v }
285
-
286
- http.request(request) do |response|
287
- unless response.is_a?(Net::HTTPSuccess) && response['content-type']&.start_with?('text/event-stream')
288
- @mutex.synchronize do
289
- @connection_established = false
290
- @connection_cv.broadcast
291
- end
292
- raise MCPClient::Errors::ServerError, 'Server response not OK or not text/event-stream'
293
- end
261
+ uri = URI.parse(@base_url)
262
+ sse_base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
263
+ sse_path = uri.request_uri
294
264
 
295
- @mutex.synchronize do
296
- @sse_connected = true
297
- 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
298
270
 
299
- response.read_body do |chunk|
300
- @logger.debug("SSE chunk received: #{chunk.inspect}")
301
- process_sse_chunk(chunk.dup)
302
- end
303
- end
304
- end
305
- rescue StandardError
306
- nil
307
- ensure
308
- sse_http&.finish if sse_http&.started?
309
- @mutex.synchronize do
310
- @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)
311
276
  end
312
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 }
313
286
  end
314
287
  end
315
288
 
@@ -339,14 +312,11 @@ module MCPClient
339
312
 
340
313
  case event[:event]
341
314
  when 'endpoint'
342
- if event[:data].include?('sessionId=')
343
- session_id = event[:data].split('sessionId=').last
344
-
345
- @mutex.synchronize do
346
- @session_id = session_id
347
- @connection_established = true
348
- @connection_cv.broadcast
349
- end
315
+ ep = event[:data]
316
+ @mutex.synchronize do
317
+ @rpc_endpoint = ep
318
+ @connection_established = true
319
+ @connection_cv.broadcast
350
320
  end
351
321
  when 'message'
352
322
  begin
@@ -462,78 +432,56 @@ module MCPClient
462
432
  def send_jsonrpc_request(request)
463
433
  @logger.debug("Sending JSON-RPC request: #{request.to_json}")
464
434
  uri = URI.parse(@base_url)
465
- rpc_http = Net::HTTP.new(uri.host, uri.port)
466
-
467
- if uri.scheme == 'https'
468
- rpc_http.use_ssl = true
469
- 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
470
443
  end
471
444
 
472
- rpc_http.open_timeout = 10
473
- rpc_http.read_timeout = @read_timeout
474
- rpc_http.keep_alive_timeout = 60
475
-
476
- begin
477
- rpc_http.start do |http|
478
- session_id = @mutex.synchronize { @session_id }
479
-
480
- url = if session_id
481
- "#{@base_url.sub(%r{/sse/?$}, '')}/messages?sessionId=#{session_id}"
482
- else
483
- "#{@base_url.sub(%r{/sse/?$}, '')}/messages"
484
- end
485
-
486
- uri = URI.parse(url)
487
- http_request = Net::HTTP::Post.new(uri)
488
- http_request.content_type = 'application/json'
489
- http_request.body = request.to_json
490
-
491
- headers = @mutex.synchronize { @headers.dup }
492
- headers.except('Accept', 'Cache-Control')
493
- .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}")
494
457
 
495
- response = http.request(http_request)
496
- @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
497
461
 
498
- unless response.is_a?(Net::HTTPSuccess)
499
- 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)
500
471
  end
472
+ return result if result
473
+ break if Time.now - start_time > timeout
501
474
 
502
- if response.code == '202'
503
- request_id = request[:id]
504
-
505
- start_time = Time.now
506
- timeout = 10
507
- result = nil
508
-
509
- loop do
510
- @mutex.synchronize do
511
- if @sse_results[request_id]
512
- result = @sse_results[request_id]
513
- @sse_results.delete(request_id)
514
- end
515
- end
516
-
517
- break if result || (Time.now - start_time > timeout)
518
-
519
- sleep 0.1
520
- end
521
-
522
- return result if result
523
-
524
- raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
525
-
526
- else
527
- begin
528
- data = JSON.parse(response.body)
529
- return data['result']
530
- rescue JSON::ParserError => e
531
- raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
532
- end
533
- 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}"
534
484
  end
535
- ensure
536
- rpc_http.finish if rpc_http.started?
537
485
  end
538
486
  end
539
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.0'
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.0
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
  - - ">="