ruby-mcp-client 0.7.2 → 0.7.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 +4 -4
- data/README.md +15 -15
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +26 -12
- data/lib/mcp_client/server_streamable_http.rb +277 -9
- data/lib/mcp_client/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd6b4269c644ed783b7d574e5a60b26ef0c749c3fe0a58fc86b4d7d5d8786a0d
|
4
|
+
data.tar.gz: 58eb4935ec413db8478dc29983f5baa68d7c28627968ffb5189d1b9754090a51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee86e5f1e58fb98df9a9a7e715049eb8d7ae5f182430060633c9f97d7b2268d1a02859403ace69f6a400344cffad80dd9238169a638e27491a6bc99ce70fb462
|
7
|
+
data.tar.gz: 27e82788b84bc0887c273ab3c1277180e85f4930f413500fab4dbf2a7474dcb151bf510ec2509caf7a95bf53311dee587e65d8826b226e7ef164d9acd640582a
|
data/README.md
CHANGED
@@ -288,12 +288,12 @@ require 'logger'
|
|
288
288
|
logger = Logger.new($stdout)
|
289
289
|
logger.level = Logger::INFO
|
290
290
|
|
291
|
-
# Create an MCP client that connects to a Playwright MCP server via
|
291
|
+
# Create an MCP client that connects to a Playwright MCP server via Streamable HTTP
|
292
292
|
# First run: npx @playwright/mcp@latest --port 8931
|
293
|
-
|
293
|
+
mcp_client = MCPClient.create_client(
|
294
294
|
mcp_server_configs: [
|
295
|
-
MCPClient.
|
296
|
-
base_url: 'http://localhost:8931/
|
295
|
+
MCPClient.streamable_http_config(
|
296
|
+
base_url: 'http://localhost:8931/mcp',
|
297
297
|
read_timeout: 30, # Timeout in seconds for request fulfillment
|
298
298
|
ping: 10, # Send ping after 10 seconds of inactivity
|
299
299
|
# Connection closes automatically after inactivity (2.5x ping interval)
|
@@ -304,33 +304,33 @@ sse_client = MCPClient.create_client(
|
|
304
304
|
)
|
305
305
|
|
306
306
|
# List available tools
|
307
|
-
tools =
|
307
|
+
tools = mcp_client.list_tools
|
308
308
|
|
309
309
|
# Launch a browser
|
310
|
-
result =
|
311
|
-
result =
|
310
|
+
result = mcp_client.call_tool('browser_install', {})
|
311
|
+
result = mcp_client.call_tool('browser_navigate', { url: 'about:blank' })
|
312
312
|
# No browser ID needed with these tool names
|
313
313
|
|
314
314
|
# Create a new page
|
315
|
-
page_result =
|
315
|
+
page_result = mcp_client.call_tool('browser_tab', {action: 'create'})
|
316
316
|
# No page ID needed with these tool names
|
317
317
|
|
318
318
|
# Navigate to a website
|
319
|
-
|
319
|
+
mcp_client.call_tool('browser_navigate', { url: 'https://example.com' })
|
320
320
|
|
321
321
|
# Get page title
|
322
|
-
title_result =
|
322
|
+
title_result = mcp_client.call_tool('browser_snapshot', {})
|
323
323
|
puts "Page snapshot: #{title_result}"
|
324
324
|
|
325
325
|
# Take a screenshot
|
326
|
-
screenshot_result =
|
326
|
+
screenshot_result = mcp_client.call_tool('browser_take_screenshot', {})
|
327
327
|
|
328
328
|
# Ping the server to verify connectivity
|
329
|
-
ping_result =
|
329
|
+
ping_result = mcp_client.ping
|
330
330
|
puts "Ping successful: #{ping_result.inspect}"
|
331
331
|
|
332
332
|
# Clean up
|
333
|
-
|
333
|
+
mcp_client.cleanup
|
334
334
|
```
|
335
335
|
|
336
336
|
See `examples/mcp_sse_server_example.rb` for the full Playwright SSE example.
|
@@ -485,7 +485,7 @@ You can define MCP server configurations in JSON files for easier management:
|
|
485
485
|
"mcpServers": {
|
486
486
|
"playwright": {
|
487
487
|
"type": "sse",
|
488
|
-
"url": "http://localhost:8931/
|
488
|
+
"url": "http://localhost:8931/mcp",
|
489
489
|
"headers": {
|
490
490
|
"Authorization": "Bearer TOKEN"
|
491
491
|
}
|
@@ -517,7 +517,7 @@ A simpler example used in the Playwright demo (found in `examples/sample_server_
|
|
517
517
|
{
|
518
518
|
"mcpServers": {
|
519
519
|
"playwright": {
|
520
|
-
"url": "http://localhost:8931/
|
520
|
+
"url": "http://localhost:8931/mcp",
|
521
521
|
"headers": {},
|
522
522
|
"comment": "Local Playwright MCP Server running on port 8931"
|
523
523
|
}
|
@@ -45,30 +45,44 @@ module MCPClient
|
|
45
45
|
# @return [Hash] the parsed JSON data
|
46
46
|
# @raise [MCPClient::Errors::TransportError] if no data found in SSE response
|
47
47
|
def parse_sse_response(sse_body)
|
48
|
-
# Extract JSON data
|
48
|
+
# Extract JSON data from SSE format, processing events separately
|
49
49
|
# SSE format: event: message\nid: 123\ndata: {...}\n\n
|
50
|
-
|
51
|
-
|
50
|
+
events = []
|
51
|
+
current_event = { type: nil, data_lines: [], id: nil }
|
52
52
|
|
53
53
|
sse_body.lines.each do |line|
|
54
54
|
line = line.strip
|
55
|
-
|
56
|
-
|
55
|
+
|
56
|
+
if line.empty?
|
57
|
+
# Empty line marks end of an event
|
58
|
+
events << current_event.dup if current_event[:type] && !current_event[:data_lines].empty?
|
59
|
+
current_event = { type: nil, data_lines: [], id: nil }
|
60
|
+
elsif line.start_with?('event:')
|
61
|
+
current_event[:type] = line.sub(/^event:\s*/, '').strip
|
62
|
+
elsif line.start_with?('data:')
|
63
|
+
current_event[:data_lines] << line.sub(/^data:\s*/, '').strip
|
57
64
|
elsif line.start_with?('id:')
|
58
|
-
|
65
|
+
current_event[:id] = line.sub(/^id:\s*/, '').strip
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
|
69
|
+
# Handle last event if no trailing empty line
|
70
|
+
events << current_event if current_event[:type] && !current_event[:data_lines].empty?
|
71
|
+
|
72
|
+
# Find the first 'message' event which contains the JSON-RPC response
|
73
|
+
message_event = events.find { |e| e[:type] == 'message' }
|
74
|
+
|
75
|
+
raise MCPClient::Errors::TransportError, 'No data found in SSE response' unless message_event
|
76
|
+
raise MCPClient::Errors::TransportError, 'No data found in message event' if message_event[:data_lines].empty?
|
63
77
|
|
64
|
-
# Track the
|
65
|
-
if
|
66
|
-
@last_event_id =
|
67
|
-
@logger.debug("Tracking event ID for resumability: #{
|
78
|
+
# Track the event ID for resumability
|
79
|
+
if message_event[:id] && !message_event[:id].empty?
|
80
|
+
@last_event_id = message_event[:id]
|
81
|
+
@logger.debug("Tracking event ID for resumability: #{message_event[:id]}")
|
68
82
|
end
|
69
83
|
|
70
84
|
# Join multiline data fields according to SSE spec
|
71
|
-
json_data = data_lines.join("\n")
|
85
|
+
json_data = message_event[:data_lines].join("\n")
|
72
86
|
JSON.parse(json_data)
|
73
87
|
end
|
74
88
|
end
|
@@ -9,9 +9,15 @@ require 'faraday/retry'
|
|
9
9
|
require 'faraday/follow_redirects'
|
10
10
|
|
11
11
|
module MCPClient
|
12
|
-
# Implementation of MCP server that communicates via Streamable HTTP transport
|
13
|
-
# This transport uses HTTP POST
|
14
|
-
#
|
12
|
+
# Implementation of MCP server that communicates via Streamable HTTP transport (MCP 2025-03-26)
|
13
|
+
# This transport uses HTTP POST for RPC calls with optional SSE responses, and GET for event streams
|
14
|
+
# Compliant with MCP specification version 2025-03-26
|
15
|
+
#
|
16
|
+
# Key features:
|
17
|
+
# - Supports server-sent events (SSE) for real-time notifications
|
18
|
+
# - Handles ping/pong keepalive mechanism
|
19
|
+
# - Thread-safe connection management
|
20
|
+
# - Automatic reconnection with exponential backoff
|
15
21
|
class ServerStreamableHTTP < ServerBase
|
16
22
|
require_relative 'server_streamable_http/json_rpc_transport'
|
17
23
|
|
@@ -21,6 +27,12 @@ module MCPClient
|
|
21
27
|
DEFAULT_READ_TIMEOUT = 30
|
22
28
|
DEFAULT_MAX_RETRIES = 3
|
23
29
|
|
30
|
+
# SSE connection settings
|
31
|
+
SSE_CONNECTION_TIMEOUT = 300 # 5 minutes
|
32
|
+
SSE_RECONNECT_DELAY = 1 # Initial reconnect delay in seconds
|
33
|
+
SSE_MAX_RECONNECT_DELAY = 30 # Maximum reconnect delay in seconds
|
34
|
+
THREAD_JOIN_TIMEOUT = 5 # Timeout for thread cleanup
|
35
|
+
|
24
36
|
# @!attribute [r] base_url
|
25
37
|
# @return [String] The base URL of the MCP server
|
26
38
|
# @!attribute [r] endpoint
|
@@ -95,6 +107,11 @@ module MCPClient
|
|
95
107
|
@session_id = nil
|
96
108
|
@last_event_id = nil
|
97
109
|
@oauth_provider = opts[:oauth_provider]
|
110
|
+
|
111
|
+
# SSE events connection state
|
112
|
+
@events_connection = nil
|
113
|
+
@events_thread = nil
|
114
|
+
@buffer = '' # Buffer for partial SSE event data
|
98
115
|
end
|
99
116
|
|
100
117
|
# Connect to the MCP server over Streamable HTTP
|
@@ -115,6 +132,9 @@ module MCPClient
|
|
115
132
|
# Perform MCP initialization handshake
|
116
133
|
perform_initialize
|
117
134
|
|
135
|
+
# Start long-lived GET connection for server events
|
136
|
+
start_events_connection
|
137
|
+
|
118
138
|
@mutex.synchronize do
|
119
139
|
@connection_established = true
|
120
140
|
@initialized = true
|
@@ -170,7 +190,8 @@ module MCPClient
|
|
170
190
|
def call_tool(tool_name, parameters)
|
171
191
|
rpc_request('tools/call', {
|
172
192
|
name: tool_name,
|
173
|
-
arguments: parameters
|
193
|
+
arguments: parameters.except(:_meta),
|
194
|
+
**parameters.slice(:_meta)
|
174
195
|
})
|
175
196
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
176
197
|
# Re-raise connection/transport errors directly to match test expectations
|
@@ -238,28 +259,60 @@ module MCPClient
|
|
238
259
|
end
|
239
260
|
|
240
261
|
# Clean up the server connection
|
241
|
-
# Properly closes HTTP connections and clears cached state
|
262
|
+
# Properly closes HTTP connections, stops threads, and clears cached state
|
242
263
|
def cleanup
|
243
264
|
@mutex.synchronize do
|
244
|
-
|
245
|
-
|
265
|
+
return unless @connection_established || @initialized
|
266
|
+
|
267
|
+
@logger.info('Cleaning up Streamable HTTP connection')
|
246
268
|
|
269
|
+
# Mark connection as closed to stop reconnection attempts
|
247
270
|
@connection_established = false
|
248
271
|
@initialized = false
|
249
272
|
|
250
|
-
|
273
|
+
# Attempt to terminate session before cleanup
|
274
|
+
begin
|
275
|
+
terminate_session if @session_id
|
276
|
+
rescue StandardError => e
|
277
|
+
@logger.warn("Failed to terminate session: #{e.message}")
|
278
|
+
end
|
251
279
|
|
252
|
-
#
|
280
|
+
# Stop events thread gracefully
|
281
|
+
if @events_thread&.alive?
|
282
|
+
@logger.debug('Stopping events thread...')
|
283
|
+
@events_thread.kill
|
284
|
+
@events_thread.join(THREAD_JOIN_TIMEOUT)
|
285
|
+
end
|
286
|
+
@events_thread = nil
|
287
|
+
|
288
|
+
# Clear connections and state
|
253
289
|
@http_conn = nil
|
290
|
+
@events_connection = nil
|
254
291
|
@session_id = nil
|
292
|
+
@last_event_id = nil
|
255
293
|
|
294
|
+
# Clear cached data
|
256
295
|
@tools = nil
|
257
296
|
@tools_data = nil
|
297
|
+
@buffer = ''
|
298
|
+
|
299
|
+
@logger.info('Cleanup completed')
|
258
300
|
end
|
259
301
|
end
|
260
302
|
|
261
303
|
private
|
262
304
|
|
305
|
+
def perform_initialize
|
306
|
+
super
|
307
|
+
# Send initialized notification to acknowledge completion of initialization
|
308
|
+
notification = build_jsonrpc_notification('notifications/initialized', {})
|
309
|
+
begin
|
310
|
+
send_http_request(notification)
|
311
|
+
rescue MCPClient::Errors::ServerError, MCPClient::Errors::ConnectionError, Faraday::ConnectionFailed => e
|
312
|
+
raise MCPClient::Errors::TransportError, "Failed to send initialized notification: #{e.message}"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
263
316
|
# Default options for server initialization
|
264
317
|
# @return [Hash] Default options
|
265
318
|
def default_options
|
@@ -327,5 +380,220 @@ module MCPClient
|
|
327
380
|
|
328
381
|
raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
|
329
382
|
end
|
383
|
+
|
384
|
+
# Start the long-lived GET connection for server events
|
385
|
+
# Creates a separate thread to maintain SSE connection for server notifications
|
386
|
+
# @return [void]
|
387
|
+
def start_events_connection
|
388
|
+
return if @events_thread&.alive?
|
389
|
+
|
390
|
+
@logger.info('Starting SSE events connection thread')
|
391
|
+
@events_thread = Thread.new do
|
392
|
+
Thread.current.name = 'MCP-SSE-Events'
|
393
|
+
Thread.current.report_on_exception = false # We handle exceptions internally
|
394
|
+
|
395
|
+
begin
|
396
|
+
handle_events_connection
|
397
|
+
rescue StandardError => e
|
398
|
+
@logger.error("Events thread crashed: #{e.message}")
|
399
|
+
@logger.debug(e.backtrace.join("\n")) if @logger.level <= Logger::DEBUG
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Handle the events connection in a separate thread
|
405
|
+
# Maintains a persistent SSE connection for server notifications and ping/pong
|
406
|
+
# @return [void]
|
407
|
+
def handle_events_connection
|
408
|
+
reconnect_delay = SSE_RECONNECT_DELAY
|
409
|
+
|
410
|
+
loop do
|
411
|
+
# Create a Faraday connection specifically for SSE streaming
|
412
|
+
# Using net_http adapter for better streaming support
|
413
|
+
conn = Faraday.new(url: @base_url) do |f|
|
414
|
+
f.request :retry, max: 0 # No automatic retries for SSE stream
|
415
|
+
f.options.open_timeout = 10
|
416
|
+
f.options.timeout = SSE_CONNECTION_TIMEOUT
|
417
|
+
f.adapter :net_http do |http|
|
418
|
+
http.read_timeout = SSE_CONNECTION_TIMEOUT
|
419
|
+
http.open_timeout = 10
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
@logger.debug("Establishing SSE events connection to #{@endpoint}") if @logger.level <= Logger::DEBUG
|
424
|
+
|
425
|
+
response = conn.get(@endpoint) do |req|
|
426
|
+
apply_events_headers(req)
|
427
|
+
|
428
|
+
# Handle streaming response with on_data callback
|
429
|
+
req.options.on_data = proc do |chunk, _total_bytes|
|
430
|
+
if chunk && !chunk.empty?
|
431
|
+
@logger.debug("Received event chunk (#{chunk.bytesize} bytes)") if @logger.level <= Logger::DEBUG
|
432
|
+
process_event_chunk(chunk)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
@logger.debug("Events connection completed with status: #{response.status}") if @logger.level <= Logger::DEBUG
|
438
|
+
|
439
|
+
# Connection closed normally, check if we should reconnect
|
440
|
+
break unless @mutex.synchronize { @connection_established }
|
441
|
+
|
442
|
+
@logger.info('Events connection closed, reconnecting...')
|
443
|
+
sleep reconnect_delay
|
444
|
+
reconnect_delay = [reconnect_delay * 2, SSE_MAX_RECONNECT_DELAY].min
|
445
|
+
|
446
|
+
# Intentional shutdown
|
447
|
+
rescue Net::ReadTimeout, Faraday::TimeoutError
|
448
|
+
# Timeout after inactivity - this is expected for long-lived connections
|
449
|
+
break unless @mutex.synchronize { @connection_established }
|
450
|
+
|
451
|
+
@logger.debug('Events connection timed out after inactivity, reconnecting...')
|
452
|
+
sleep reconnect_delay
|
453
|
+
rescue Faraday::ConnectionFailed => e
|
454
|
+
break unless @mutex.synchronize { @connection_established }
|
455
|
+
|
456
|
+
@logger.warn("Events connection failed: #{e.message}, retrying in #{reconnect_delay}s...")
|
457
|
+
sleep reconnect_delay
|
458
|
+
reconnect_delay = [reconnect_delay * 2, SSE_MAX_RECONNECT_DELAY].min
|
459
|
+
rescue StandardError => e
|
460
|
+
break unless @mutex.synchronize { @connection_established }
|
461
|
+
|
462
|
+
@logger.error("Unexpected error in events connection: #{e.class} - #{e.message}")
|
463
|
+
@logger.debug(e.backtrace.join("\n")) if @logger.level <= Logger::DEBUG
|
464
|
+
sleep reconnect_delay
|
465
|
+
reconnect_delay = [reconnect_delay * 2, SSE_MAX_RECONNECT_DELAY].min
|
466
|
+
end
|
467
|
+
ensure
|
468
|
+
@logger.info('Events connection thread terminated')
|
469
|
+
end
|
470
|
+
|
471
|
+
# Apply headers for events connection
|
472
|
+
# @param req [Faraday::Request] HTTP request
|
473
|
+
def apply_events_headers(req)
|
474
|
+
@headers.each { |k, v| req.headers[k] = v }
|
475
|
+
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
476
|
+
end
|
477
|
+
|
478
|
+
# Process event chunks from the server
|
479
|
+
# Buffers partial chunks and processes complete SSE events
|
480
|
+
# @param chunk [String] the chunk to process
|
481
|
+
def process_event_chunk(chunk)
|
482
|
+
@logger.debug("Processing event chunk: #{chunk.inspect}") if @logger.level <= Logger::DEBUG
|
483
|
+
|
484
|
+
@mutex.synchronize do
|
485
|
+
@buffer += chunk
|
486
|
+
|
487
|
+
# Extract complete events (SSE format: events end with double newline)
|
488
|
+
while (event_end = @buffer.index("\n\n") || @buffer.index("\r\n\r\n"))
|
489
|
+
event_data = extract_event(event_end)
|
490
|
+
parse_and_handle_event(event_data)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
rescue StandardError => e
|
494
|
+
@logger.error("Error processing event chunk: #{e.message}")
|
495
|
+
@logger.debug(e.backtrace.join("\n")) if @logger.level <= Logger::DEBUG
|
496
|
+
end
|
497
|
+
|
498
|
+
# Extract a single event from the buffer
|
499
|
+
# @param event_end [Integer] the position where the event ends
|
500
|
+
# @return [String] the extracted event data
|
501
|
+
def extract_event(event_end)
|
502
|
+
# Determine the line ending style and extract accordingly
|
503
|
+
crlf_index = @buffer.index("\r\n\r\n")
|
504
|
+
lf_index = @buffer.index("\n\n")
|
505
|
+
if crlf_index && (lf_index.nil? || crlf_index < lf_index)
|
506
|
+
@buffer.slice!(0, event_end + 4) # \r\n\r\n is 4 chars
|
507
|
+
else
|
508
|
+
@buffer.slice!(0, event_end + 2) # \n\n is 2 chars
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Parse and handle an SSE event
|
513
|
+
# Parses SSE format according to the W3C specification
|
514
|
+
# @param event_data [String] the raw event data
|
515
|
+
def parse_and_handle_event(event_data)
|
516
|
+
event = { event: 'message', data: '', id: nil }
|
517
|
+
data_lines = []
|
518
|
+
|
519
|
+
event_data.each_line do |line|
|
520
|
+
line = line.chomp
|
521
|
+
next if line.empty? || line.start_with?(':') # Skip empty lines and comments
|
522
|
+
|
523
|
+
if line.start_with?('event:')
|
524
|
+
event[:event] = line[6..].strip
|
525
|
+
elsif line.start_with?('data:')
|
526
|
+
# SSE allows multiple data lines that should be joined with newlines
|
527
|
+
data_lines << line[5..].strip
|
528
|
+
elsif line.start_with?('id:')
|
529
|
+
# Track event ID for resumability (MCP future enhancement)
|
530
|
+
event[:id] = line[3..].strip
|
531
|
+
@last_event_id = event[:id]
|
532
|
+
elsif line.start_with?('retry:')
|
533
|
+
# Server can suggest reconnection delay (in milliseconds)
|
534
|
+
retry_ms = line[6..].strip.to_i
|
535
|
+
@logger.debug("Server suggested retry delay: #{retry_ms}ms") if @logger.level <= Logger::DEBUG
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
event[:data] = data_lines.join("\n")
|
540
|
+
|
541
|
+
# Only process non-empty data
|
542
|
+
handle_server_message(event[:data]) unless event[:data].empty?
|
543
|
+
end
|
544
|
+
|
545
|
+
# Handle server messages (notifications and requests)
|
546
|
+
# Processes ping/pong keepalive and server notifications
|
547
|
+
# @param data [String] the JSON data from SSE event
|
548
|
+
def handle_server_message(data)
|
549
|
+
return if data.empty?
|
550
|
+
|
551
|
+
begin
|
552
|
+
message = JSON.parse(data)
|
553
|
+
|
554
|
+
# Handle ping requests from server (keepalive mechanism)
|
555
|
+
if message['method'] == 'ping' && message.key?('id')
|
556
|
+
handle_ping_request(message['id'])
|
557
|
+
elsif message['method'] && !message.key?('id')
|
558
|
+
# Handle server notifications (messages without id)
|
559
|
+
@notification_callback&.call(message['method'], message['params'])
|
560
|
+
elsif message.key?('id')
|
561
|
+
# This might be a server-to-client request (future MCP versions)
|
562
|
+
@logger.warn("Received unhandled server request: #{message['method']}")
|
563
|
+
end
|
564
|
+
rescue JSON::ParserError => e
|
565
|
+
@logger.error("Invalid JSON in server message: #{e.message}")
|
566
|
+
@logger.debug("Raw data: #{data.inspect}") if @logger.level <= Logger::DEBUG
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# Handle ping request from server
|
571
|
+
# Sends pong response to maintain session keepalive
|
572
|
+
# @param ping_id [Integer, String] the ping request ID
|
573
|
+
def handle_ping_request(ping_id)
|
574
|
+
pong_response = {
|
575
|
+
jsonrpc: '2.0',
|
576
|
+
id: ping_id,
|
577
|
+
result: {}
|
578
|
+
}
|
579
|
+
|
580
|
+
# Send pong response in a separate thread to avoid blocking event processing
|
581
|
+
Thread.new do
|
582
|
+
conn = http_connection
|
583
|
+
response = conn.post(@endpoint) do |req|
|
584
|
+
@headers.each { |k, v| req.headers[k] = v }
|
585
|
+
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
586
|
+
req.body = pong_response.to_json
|
587
|
+
end
|
588
|
+
|
589
|
+
if response.success?
|
590
|
+
@logger.debug("Sent pong response for ping ID: #{ping_id}") if @logger.level <= Logger::DEBUG
|
591
|
+
else
|
592
|
+
@logger.warn("Failed to send pong response: HTTP #{response.status}")
|
593
|
+
end
|
594
|
+
rescue StandardError => e
|
595
|
+
@logger.error("Failed to send pong response: #{e.message}")
|
596
|
+
end
|
597
|
+
end
|
330
598
|
end
|
331
599
|
end
|
data/lib/mcp_client/version.rb
CHANGED
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.7.
|
4
|
+
version: 0.7.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-
|
11
|
+
date: 2025-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|