ruby-mcp-client 0.5.2 → 0.5.3

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: fbd3caf1e80f6c2caf625495072211b827ae08e3542fa33306537918d0b6eb5f
4
- data.tar.gz: 1eea548ba10e7de3fec68a92d449e4552fde3c5920af045e2e828925dba2aad7
3
+ metadata.gz: 2ec468ae4d2033287bfc89a788e87acc878ab833552e98bef88c95176fa6a906
4
+ data.tar.gz: d6f2f676be6932d8dbe641df40674aa77ef64d1296d9fdf4eb7c892537f2fa0c
5
5
  SHA512:
6
- metadata.gz: e8eae783bd1e6a4db013a934e0686641befaec435fb288531ce85899efc0a66023d3d174dedffae21f75089f406f6368f286f13644701fbdee747492b795535b
7
- data.tar.gz: 534cc5c88f8f3737c5fc3e684174980af74ac18398532e4b5698490b451b5fc5ff0831ca48639119445c71f4afa94ba3fca94ff81edfa03d1e3ba34a73c4f1b1
6
+ metadata.gz: 49f5427d6ffc9bc5632bed9b56e4c03e70ce4894d087331f12fd340b60ce82a539b9b2a362c1ce8d0b9686d9d9a4dae4bea3d11ec732b8a0ae34a3e898ff43b2
7
+ data.tar.gz: 3e32823ef422661fbcf507caf141d0ce65aedd0661ecb54f3b2a287565b630049d2acdfc4bb7b9a93a965e75403b635e43eb21259f035d8b82810f58e0fff304
data/README.md CHANGED
@@ -36,6 +36,7 @@ with popular AI services with built-in conversions:
36
36
 
37
37
  - `to_openai_tools()` - Formats tools for OpenAI API
38
38
  - `to_anthropic_tools()` - Formats tools for Anthropic Claude API
39
+ - `to_google_tools()` - Formats tools for Google Vertex AI API (automatically removes "$schema" keys not accepted by Vertex AI)
39
40
 
40
41
  ## Usage
41
42
 
@@ -53,6 +54,8 @@ client = MCPClient.create_client(
53
54
  base_url: 'https://api.example.com/sse',
54
55
  headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
55
56
  read_timeout: 30, # Optional timeout in seconds (default: 30)
57
+ ping: 10, # Optional ping interval in seconds of inactivity (default: 10)
58
+ # Connection closes automatically after inactivity (2.5x ping interval)
56
59
  retries: 3, # Optional number of retry attempts (default: 0)
57
60
  retry_backoff: 1 # Optional backoff delay in seconds (default: 1)
58
61
  # Native support for tool streaming via call_tool_streaming method
@@ -98,6 +101,7 @@ end
98
101
  # Format tools for specific AI services
99
102
  openai_tools = client.to_openai_tools
100
103
  anthropic_tools = client.to_anthropic_tools
104
+ google_tools = client.to_google_tools
101
105
 
102
106
  # Register for server notifications
103
107
  client.on_notification do |server, method, params|
@@ -139,7 +143,10 @@ sse_client = MCPClient.create_client(
139
143
  mcp_server_configs: [
140
144
  MCPClient.sse_config(
141
145
  base_url: 'http://localhost:8931/sse',
142
- read_timeout: 30, # Timeout in seconds
146
+ read_timeout: 30, # Timeout in seconds for request fulfillment
147
+ ping: 10, # Send ping after 10 seconds of inactivity
148
+ # Connection closes automatically after inactivity (2.5x ping interval)
149
+ retries: 2 # Number of retry attempts on transient errors
143
150
  )
144
151
  ]
145
152
  )
@@ -262,6 +269,7 @@ Complete examples can be found in the `examples/` directory:
262
269
  - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
263
270
  - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
264
271
  - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
272
+ - `gemini_ai_mcp.rb` - Integration with Google Vertex AI and Gemini models
265
273
  - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
266
274
 
267
275
  ## MCP Server Compatibility
@@ -349,6 +357,11 @@ Special configuration options:
349
357
  The SSE client implementation provides these key features:
350
358
 
351
359
  - **Robust connection handling**: Properly manages HTTP/HTTPS connections with configurable timeouts and retries
360
+ - **Advanced connection management**:
361
+ - **Inactivity tracking**: Monitors connection activity to detect idle connections
362
+ - **Automatic ping**: Sends ping requests after a configurable period of inactivity (default: 10 seconds)
363
+ - **Automatic disconnection**: Closes idle connections after inactivity (2.5× ping interval)
364
+ - **MCP compliant**: Any server communication resets the inactivity timer per specification
352
365
  - **Thread safety**: All operations are thread-safe using monitors and synchronized access
353
366
  - **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
354
367
  - **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport with initialize handshake
@@ -411,4 +424,4 @@ This gem is available as open source under the [MIT License](LICENSE).
411
424
  ## Contributing
412
425
 
413
426
  Bug reports and pull requests are welcome on GitHub at
414
- https://github.com/simonx1/ruby-mcp-client.
427
+ https://github.com/simonx1/ruby-mcp-client.
@@ -94,6 +94,12 @@ module MCPClient
94
94
  tools.map(&:to_anthropic_tool)
95
95
  end
96
96
 
97
+ def to_google_tools(tool_names: nil)
98
+ tools = list_tools
99
+ tools = tools.select { |t| tool_names.include?(t.name) } if tool_names
100
+ tools.map(&:to_google_tool)
101
+ end
102
+
97
103
  # Clean up all server connections
98
104
  def cleanup
99
105
  servers.each(&:cleanup)
@@ -21,6 +21,7 @@ module MCPClient
21
21
  base_url: config[:base_url],
22
22
  headers: config[:headers] || {},
23
23
  read_timeout: config[:read_timeout] || 30,
24
+ ping: config[:ping] || 10,
24
25
  retries: config[:retries] || 0,
25
26
  retry_backoff: config[:retry_backoff] || 1,
26
27
  logger: config[:logger]
@@ -11,15 +11,20 @@ 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
+ # Ratio of close_after timeout to ping interval
15
+ CLOSE_AFTER_PING_RATIO = 2.5
16
+
14
17
  attr_reader :base_url, :tools, :server_info, :capabilities
15
18
 
16
19
  # @param base_url [String] The base URL of the MCP server
17
20
  # @param headers [Hash] Additional headers to include in requests
18
21
  # @param read_timeout [Integer] Read timeout in seconds (default: 30)
22
+ # @param ping [Integer] Time in seconds after which to send ping if no activity (default: 10)
19
23
  # @param retries [Integer] number of retry attempts on transient errors
20
24
  # @param retry_backoff [Numeric] base delay in seconds for exponential backoff
21
25
  # @param logger [Logger, nil] optional logger
22
- def initialize(base_url:, headers: {}, read_timeout: 30, retries: 0, retry_backoff: 1, logger: nil)
26
+ def initialize(base_url:, headers: {}, read_timeout: 30, ping: 10,
27
+ retries: 0, retry_backoff: 1, logger: nil)
23
28
  super()
24
29
  @logger = logger || Logger.new($stdout, level: Logger::WARN)
25
30
  @logger.progname = self.class.name
@@ -36,6 +41,9 @@ module MCPClient
36
41
  # HTTP client is managed via Faraday
37
42
  @tools = nil
38
43
  @read_timeout = read_timeout
44
+ @ping_interval = ping
45
+ # Set close_after to a multiple of the ping interval
46
+ @close_after = (ping * CLOSE_AFTER_PING_RATIO).to_i
39
47
 
40
48
  # SSE-provided JSON-RPC endpoint path for POST requests
41
49
  @rpc_endpoint = nil
@@ -51,6 +59,10 @@ module MCPClient
51
59
  @auth_error = nil
52
60
  # Whether to use SSE transport; may disable if handshake fails
53
61
  @use_sse = true
62
+
63
+ # Time of last activity
64
+ @last_activity_time = Time.now
65
+ @activity_timer_thread = nil
54
66
  end
55
67
 
56
68
  # Stream tool call fallback for SSE transport (yields single result)
@@ -138,6 +150,7 @@ module MCPClient
138
150
  start_sse_thread
139
151
  effective_timeout = [@read_timeout || 30, 30].min
140
152
  wait_for_connection(timeout: effective_timeout)
153
+ start_activity_monitor
141
154
  true
142
155
  rescue MCPClient::Errors::ConnectionError => e
143
156
  cleanup
@@ -169,6 +182,13 @@ module MCPClient
169
182
  end
170
183
  @sse_thread = nil
171
184
 
185
+ begin
186
+ @activity_timer_thread&.kill
187
+ rescue StandardError
188
+ nil
189
+ end
190
+ @activity_timer_thread = nil
191
+
172
192
  if @http_client
173
193
  @http_client.finish if @http_client.started?
174
194
  @http_client = nil
@@ -227,8 +247,56 @@ module MCPClient
227
247
  raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
228
248
  end
229
249
 
250
+ # Ping the server to keep the connection alive
251
+ # @return [Hash] the result of the ping request
252
+ def ping
253
+ rpc_request('ping')
254
+ end
255
+
230
256
  private
231
257
 
258
+ # Start the activity monitor thread
259
+ # This thread monitors connection activity and:
260
+ # 1. Sends a ping if there's no activity for @ping_interval seconds
261
+ # 2. Closes the connection if there's no activity for @close_after seconds
262
+ def start_activity_monitor
263
+ return if @activity_timer_thread&.alive?
264
+
265
+ @mutex.synchronize { @last_activity_time = Time.now }
266
+
267
+ @activity_timer_thread = Thread.new do
268
+ loop do
269
+ sleep 1 # Check every second
270
+
271
+ last_activity = nil
272
+ @mutex.synchronize { last_activity = @last_activity_time }
273
+
274
+ time_since_activity = Time.now - last_activity
275
+
276
+ if @close_after && time_since_activity >= @close_after
277
+ @logger.info("Closing connection due to inactivity (#{time_since_activity.round(1)}s)")
278
+ cleanup
279
+ break
280
+ elsif @ping_interval && time_since_activity >= @ping_interval
281
+ begin
282
+ @logger.debug("Sending ping after #{time_since_activity.round(1)}s of inactivity")
283
+ ping
284
+ @mutex.synchronize { @last_activity_time = Time.now }
285
+ rescue StandardError => e
286
+ @logger.error("Error sending ping: #{e.message}")
287
+ end
288
+ end
289
+ end
290
+ rescue StandardError => e
291
+ @logger.error("Activity monitor error: #{e.message}")
292
+ end
293
+ end
294
+
295
+ # Record activity to reset the inactivity timer
296
+ def record_activity
297
+ @mutex.synchronize { @last_activity_time = Time.now }
298
+ end
299
+
232
300
  # Wait for SSE connection to be established with periodic checks
233
301
  # @param timeout [Integer] Maximum time to wait in seconds
234
302
  # @raise [MCPClient::Errors::ConnectionError] if timeout expires
@@ -368,6 +436,9 @@ module MCPClient
368
436
  def process_sse_chunk(chunk)
369
437
  @logger.debug("Processing SSE chunk: #{chunk.inspect}")
370
438
 
439
+ # Only record activity for real events
440
+ record_activity if chunk.include?('event:')
441
+
371
442
  # Check for direct JSON error responses (which aren't proper SSE events)
372
443
  if chunk.start_with?('{') && chunk.include?('"error"') &&
373
444
  (chunk.include?('Unauthorized') || chunk.include?('authentication'))
@@ -619,6 +690,10 @@ module MCPClient
619
690
  # @return [Hash] the result of the request
620
691
  def send_jsonrpc_request(request)
621
692
  @logger.debug("Sending JSON-RPC request: #{request.to_json}")
693
+
694
+ # Record activity when sending a request
695
+ record_activity
696
+
622
697
  uri = URI.parse(@base_url)
623
698
  base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
624
699
  rpc_ep = @mutex.synchronize { @rpc_endpoint }
@@ -643,6 +718,9 @@ module MCPClient
643
718
  end
644
719
  @logger.debug("Received JSON-RPC response: #{response.status} #{response.body}")
645
720
 
721
+ # Record activity when receiving a response
722
+ record_activity
723
+
646
724
  unless response.success?
647
725
  raise MCPClient::Errors::ServerError, "Server returned error: #{response.status} #{response.reason_phrase}"
648
726
  end
@@ -651,17 +729,32 @@ module MCPClient
651
729
  # Wait for result via SSE channel
652
730
  request_id = request[:id]
653
731
  start_time = Time.now
732
+ # Use the specified read_timeout for the overall operation
654
733
  timeout = @read_timeout || 10
734
+
735
+ # Check every 100ms for the result, with a total timeout from read_timeout
655
736
  loop do
656
737
  result = nil
657
738
  @mutex.synchronize do
658
739
  result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
659
740
  end
660
- return result if result
661
- break if Time.now - start_time > timeout
662
741
 
742
+ if result
743
+ # Record activity when receiving a result
744
+ record_activity
745
+ return result
746
+ end
747
+
748
+ current_time = Time.now
749
+ time_elapsed = current_time - start_time
750
+
751
+ # If we've exceeded the timeout, raise an error
752
+ break if time_elapsed > timeout
753
+
754
+ # Sleep for a short time before checking again
663
755
  sleep 0.1
664
756
  end
757
+
665
758
  raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
666
759
  else
667
760
  begin
@@ -46,5 +46,33 @@ module MCPClient
46
46
  input_schema: @schema
47
47
  }
48
48
  end
49
+
50
+ def to_google_tool
51
+ {
52
+ name: @name,
53
+ description: @description,
54
+ parameters: cleaned_schema(@schema)
55
+ }
56
+ end
57
+
58
+ private
59
+
60
+ # Recursively remove "$schema" keys that are not accepted by Vertex AI
61
+ # @param obj [Object] schema element (Hash/Array/other)
62
+ # @return [Object] cleaned schema without "$schema" keys
63
+ def cleaned_schema(obj)
64
+ case obj
65
+ when Hash
66
+ obj.each_with_object({}) do |(k, v), h|
67
+ next if k == '$schema'
68
+
69
+ h[k] = cleaned_schema(v)
70
+ end
71
+ when Array
72
+ obj.map { |v| cleaned_schema(v) }
73
+ else
74
+ obj
75
+ end
76
+ end
49
77
  end
50
78
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.5.2'
5
+ VERSION = '0.5.3'
6
6
  end
data/lib/mcp_client.rb CHANGED
@@ -58,15 +58,17 @@ module MCPClient
58
58
  # @param base_url [String] base URL for the server
59
59
  # @param headers [Hash] HTTP headers to include in requests
60
60
  # @param read_timeout [Integer] read timeout in seconds (default: 30)
61
+ # @param ping [Integer] time in seconds after which to send ping if no activity (default: 10)
61
62
  # @param retries [Integer] number of retry attempts (default: 0)
62
63
  # @param retry_backoff [Integer] backoff delay in seconds (default: 1)
63
64
  # @return [Hash] server configuration
64
- def self.sse_config(base_url:, headers: {}, read_timeout: 30, retries: 0, retry_backoff: 1)
65
+ def self.sse_config(base_url:, headers: {}, read_timeout: 30, ping: 10, retries: 0, retry_backoff: 1)
65
66
  {
66
67
  type: 'sse',
67
68
  base_url: base_url,
68
69
  headers: headers,
69
70
  read_timeout: read_timeout,
71
+ ping: ping,
70
72
  retries: retries,
71
73
  retry_backoff: retry_backoff
72
74
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.3
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-05-09 00:00:00.000000000 Z
11
+ date: 2025-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday