ruby-mcp-client 0.9.0 → 0.9.1
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 +220 -1229
- data/lib/mcp_client/client.rb +189 -5
- data/lib/mcp_client/errors.rb +3 -0
- data/lib/mcp_client/http_transport_base.rb +7 -1
- data/lib/mcp_client/json_rpc_common.rb +7 -3
- data/lib/mcp_client/root.rb +63 -0
- data/lib/mcp_client/server_factory.rb +4 -2
- data/lib/mcp_client/server_http.rb +31 -1
- data/lib/mcp_client/server_sse.rb +130 -5
- data/lib/mcp_client/server_stdio.rb +140 -0
- data/lib/mcp_client/server_streamable_http.rb +131 -1
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +317 -4
- metadata +3 -2
|
@@ -47,6 +47,8 @@ module MCPClient
|
|
|
47
47
|
@read_timeout = read_timeout
|
|
48
48
|
@env = env || {}
|
|
49
49
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
50
|
+
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
51
|
+
@sampling_request_callback = nil # MCP 2025-06-18
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# Server info from the initialize response
|
|
@@ -340,6 +342,56 @@ module MCPClient
|
|
|
340
342
|
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
|
341
343
|
end
|
|
342
344
|
|
|
345
|
+
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
346
|
+
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
347
|
+
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
348
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
349
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
350
|
+
def complete(ref:, argument:)
|
|
351
|
+
ensure_initialized
|
|
352
|
+
req_id = next_id
|
|
353
|
+
req = {
|
|
354
|
+
'jsonrpc' => '2.0',
|
|
355
|
+
'id' => req_id,
|
|
356
|
+
'method' => 'completion/complete',
|
|
357
|
+
'params' => { 'ref' => ref, 'argument' => argument }
|
|
358
|
+
}
|
|
359
|
+
send_request(req)
|
|
360
|
+
res = wait_response(req_id)
|
|
361
|
+
if (err = res['error'])
|
|
362
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
res.dig('result', 'completion') || { 'values' => [] }
|
|
366
|
+
rescue StandardError => e
|
|
367
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
371
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
372
|
+
# 'critical', 'alert', 'emergency')
|
|
373
|
+
# @return [Hash] empty result on success
|
|
374
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
375
|
+
def log_level=(level)
|
|
376
|
+
ensure_initialized
|
|
377
|
+
req_id = next_id
|
|
378
|
+
req = {
|
|
379
|
+
'jsonrpc' => '2.0',
|
|
380
|
+
'id' => req_id,
|
|
381
|
+
'method' => 'logging/setLevel',
|
|
382
|
+
'params' => { 'level' => level }
|
|
383
|
+
}
|
|
384
|
+
send_request(req)
|
|
385
|
+
res = wait_response(req_id)
|
|
386
|
+
if (err = res['error'])
|
|
387
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
res['result'] || {}
|
|
391
|
+
rescue StandardError => e
|
|
392
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
393
|
+
end
|
|
394
|
+
|
|
343
395
|
# Register a callback for elicitation requests (MCP 2025-06-18)
|
|
344
396
|
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
345
397
|
# @return [void]
|
|
@@ -347,6 +399,20 @@ module MCPClient
|
|
|
347
399
|
@elicitation_request_callback = block
|
|
348
400
|
end
|
|
349
401
|
|
|
402
|
+
# Register a callback for roots/list requests (MCP 2025-06-18)
|
|
403
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
404
|
+
# @return [void]
|
|
405
|
+
def on_roots_list_request(&block)
|
|
406
|
+
@roots_list_request_callback = block
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Register a callback for sampling requests (MCP 2025-06-18)
|
|
410
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
411
|
+
# @return [void]
|
|
412
|
+
def on_sampling_request(&block)
|
|
413
|
+
@sampling_request_callback = block
|
|
414
|
+
end
|
|
415
|
+
|
|
350
416
|
# Handle incoming JSON-RPC request from server (MCP 2025-06-18)
|
|
351
417
|
# @param msg [Hash] the JSON-RPC request message
|
|
352
418
|
# @return [void]
|
|
@@ -360,6 +426,10 @@ module MCPClient
|
|
|
360
426
|
case method
|
|
361
427
|
when 'elicitation/create'
|
|
362
428
|
handle_elicitation_create(request_id, params)
|
|
429
|
+
when 'roots/list'
|
|
430
|
+
handle_roots_list(request_id, params)
|
|
431
|
+
when 'sampling/createMessage'
|
|
432
|
+
handle_sampling_create_message(request_id, params)
|
|
363
433
|
else
|
|
364
434
|
# Unknown request method, send error response
|
|
365
435
|
send_error_response(request_id, -32_601, "Method not found: #{method}")
|
|
@@ -388,6 +458,76 @@ module MCPClient
|
|
|
388
458
|
send_elicitation_response(request_id, result)
|
|
389
459
|
end
|
|
390
460
|
|
|
461
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
462
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
463
|
+
# @param params [Hash] the request parameters
|
|
464
|
+
# @return [void]
|
|
465
|
+
def handle_roots_list(request_id, params)
|
|
466
|
+
# If no callback is registered, return empty roots list
|
|
467
|
+
unless @roots_list_request_callback
|
|
468
|
+
@logger.debug('Received roots/list request but no callback registered, returning empty list')
|
|
469
|
+
send_roots_list_response(request_id, { 'roots' => [] })
|
|
470
|
+
return
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Call the registered callback
|
|
474
|
+
result = @roots_list_request_callback.call(request_id, params)
|
|
475
|
+
|
|
476
|
+
# Send the response back to the server
|
|
477
|
+
send_roots_list_response(request_id, result)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Handle sampling/createMessage request from server (MCP 2025-06-18)
|
|
481
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
482
|
+
# @param params [Hash] the sampling parameters
|
|
483
|
+
# @return [void]
|
|
484
|
+
def handle_sampling_create_message(request_id, params)
|
|
485
|
+
# If no callback is registered, return error
|
|
486
|
+
unless @sampling_request_callback
|
|
487
|
+
@logger.warn('Received sampling request but no callback registered, returning error')
|
|
488
|
+
send_error_response(request_id, -1, 'Sampling not supported')
|
|
489
|
+
return
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Call the registered callback
|
|
493
|
+
result = @sampling_request_callback.call(request_id, params)
|
|
494
|
+
|
|
495
|
+
# Send the response back to the server
|
|
496
|
+
send_sampling_response(request_id, result)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Send roots/list response back to server (MCP 2025-06-18)
|
|
500
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
501
|
+
# @param result [Hash] the roots list result
|
|
502
|
+
# @return [void]
|
|
503
|
+
def send_roots_list_response(request_id, result)
|
|
504
|
+
response = {
|
|
505
|
+
'jsonrpc' => '2.0',
|
|
506
|
+
'id' => request_id,
|
|
507
|
+
'result' => result
|
|
508
|
+
}
|
|
509
|
+
send_message(response)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Send sampling response back to server (MCP 2025-06-18)
|
|
513
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
514
|
+
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
515
|
+
# @return [void]
|
|
516
|
+
def send_sampling_response(request_id, result)
|
|
517
|
+
# Check if result contains an error
|
|
518
|
+
if result.is_a?(Hash) && result['error']
|
|
519
|
+
send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
|
|
520
|
+
return
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
response = {
|
|
524
|
+
'jsonrpc' => '2.0',
|
|
525
|
+
'id' => request_id,
|
|
526
|
+
'result' => result
|
|
527
|
+
}
|
|
528
|
+
send_message(response)
|
|
529
|
+
end
|
|
530
|
+
|
|
391
531
|
# Send elicitation response back to server (MCP 2025-06-18)
|
|
392
532
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
393
533
|
# @param result [Hash] the elicitation result (action and optional content)
|
|
@@ -97,6 +97,7 @@ module MCPClient
|
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
@read_timeout = opts[:read_timeout]
|
|
100
|
+
@faraday_config = opts[:faraday_config]
|
|
100
101
|
@tools = nil
|
|
101
102
|
@tools_data = nil
|
|
102
103
|
@prompts = nil
|
|
@@ -117,6 +118,8 @@ module MCPClient
|
|
|
117
118
|
@events_thread = nil
|
|
118
119
|
@buffer = '' # Buffer for partial SSE event data
|
|
119
120
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
121
|
+
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
122
|
+
@sampling_request_callback = nil # MCP 2025-06-18
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
# Connect to the MCP server over Streamable HTTP
|
|
@@ -216,6 +219,33 @@ module MCPClient
|
|
|
216
219
|
end
|
|
217
220
|
end
|
|
218
221
|
|
|
222
|
+
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
223
|
+
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
224
|
+
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
225
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
226
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
227
|
+
def complete(ref:, argument:)
|
|
228
|
+
result = rpc_request('completion/complete', { ref: ref, argument: argument })
|
|
229
|
+
result['completion'] || { 'values' => [] }
|
|
230
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
231
|
+
raise
|
|
232
|
+
rescue StandardError => e
|
|
233
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
237
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
238
|
+
# 'critical', 'alert', 'emergency')
|
|
239
|
+
# @return [Hash] empty result on success
|
|
240
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
241
|
+
def log_level=(level)
|
|
242
|
+
rpc_request('logging/setLevel', { level: level })
|
|
243
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
244
|
+
raise
|
|
245
|
+
rescue StandardError => e
|
|
246
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
247
|
+
end
|
|
248
|
+
|
|
219
249
|
# List all prompts available from the MCP server
|
|
220
250
|
# @return [Array<MCPClient::Prompt>] list of available prompts
|
|
221
251
|
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
|
@@ -459,6 +489,20 @@ module MCPClient
|
|
|
459
489
|
@elicitation_request_callback = block
|
|
460
490
|
end
|
|
461
491
|
|
|
492
|
+
# Register a callback for roots/list requests (MCP 2025-06-18)
|
|
493
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
494
|
+
# @return [void]
|
|
495
|
+
def on_roots_list_request(&block)
|
|
496
|
+
@roots_list_request_callback = block
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Register a callback for sampling requests (MCP 2025-06-18)
|
|
500
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
501
|
+
# @return [void]
|
|
502
|
+
def on_sampling_request(&block)
|
|
503
|
+
@sampling_request_callback = block
|
|
504
|
+
end
|
|
505
|
+
|
|
462
506
|
private
|
|
463
507
|
|
|
464
508
|
def perform_initialize
|
|
@@ -483,7 +527,8 @@ module MCPClient
|
|
|
483
527
|
retry_backoff: 1,
|
|
484
528
|
name: nil,
|
|
485
529
|
logger: nil,
|
|
486
|
-
oauth_provider: nil
|
|
530
|
+
oauth_provider: nil,
|
|
531
|
+
faraday_config: nil
|
|
487
532
|
}
|
|
488
533
|
end
|
|
489
534
|
|
|
@@ -629,6 +674,9 @@ module MCPClient
|
|
|
629
674
|
end
|
|
630
675
|
end
|
|
631
676
|
|
|
677
|
+
# Apply user's Faraday customizations after defaults
|
|
678
|
+
@faraday_config&.call(conn)
|
|
679
|
+
|
|
632
680
|
@logger.debug("Establishing SSE events connection to #{@endpoint}") if @logger.level <= Logger::DEBUG
|
|
633
681
|
|
|
634
682
|
response = conn.get(@endpoint) do |req|
|
|
@@ -818,6 +866,10 @@ module MCPClient
|
|
|
818
866
|
case method
|
|
819
867
|
when 'elicitation/create'
|
|
820
868
|
handle_elicitation_create(request_id, params)
|
|
869
|
+
when 'roots/list'
|
|
870
|
+
handle_roots_list(request_id, params)
|
|
871
|
+
when 'sampling/createMessage'
|
|
872
|
+
handle_sampling_create_message(request_id, params)
|
|
821
873
|
else
|
|
822
874
|
# Unknown request method, send error response
|
|
823
875
|
send_error_response(request_id, -32_601, "Method not found: #{method}")
|
|
@@ -849,6 +901,84 @@ module MCPClient
|
|
|
849
901
|
send_elicitation_response(elicitation_id, result)
|
|
850
902
|
end
|
|
851
903
|
|
|
904
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
905
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
906
|
+
# @param params [Hash] the request parameters
|
|
907
|
+
# @return [void]
|
|
908
|
+
def handle_roots_list(request_id, params)
|
|
909
|
+
# If no callback is registered, return empty roots list
|
|
910
|
+
unless @roots_list_request_callback
|
|
911
|
+
@logger.debug('Received roots/list request but no callback registered, returning empty list')
|
|
912
|
+
send_roots_list_response(request_id, { 'roots' => [] })
|
|
913
|
+
return
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Call the registered callback
|
|
917
|
+
result = @roots_list_request_callback.call(request_id, params)
|
|
918
|
+
|
|
919
|
+
# Send the response back to the server
|
|
920
|
+
send_roots_list_response(request_id, result)
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Handle sampling/createMessage request from server (MCP 2025-06-18)
|
|
924
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
925
|
+
# @param params [Hash] the sampling parameters
|
|
926
|
+
# @return [void]
|
|
927
|
+
def handle_sampling_create_message(request_id, params)
|
|
928
|
+
# If no callback is registered, return error
|
|
929
|
+
unless @sampling_request_callback
|
|
930
|
+
@logger.warn('Received sampling request but no callback registered, returning error')
|
|
931
|
+
send_error_response(request_id, -1, 'Sampling not supported')
|
|
932
|
+
return
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# Call the registered callback
|
|
936
|
+
result = @sampling_request_callback.call(request_id, params)
|
|
937
|
+
|
|
938
|
+
# Send the response back to the server
|
|
939
|
+
send_sampling_response(request_id, result)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
|
|
943
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
944
|
+
# @param result [Hash] the roots list result
|
|
945
|
+
# @return [void]
|
|
946
|
+
def send_roots_list_response(request_id, result)
|
|
947
|
+
response = {
|
|
948
|
+
'jsonrpc' => '2.0',
|
|
949
|
+
'id' => request_id,
|
|
950
|
+
'result' => result
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
# Send response via HTTP POST
|
|
954
|
+
post_jsonrpc_response(response)
|
|
955
|
+
rescue StandardError => e
|
|
956
|
+
@logger.error("Error sending roots/list response: #{e.message}")
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Send sampling response back to server via HTTP POST (MCP 2025-06-18)
|
|
960
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
961
|
+
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
962
|
+
# @return [void]
|
|
963
|
+
def send_sampling_response(request_id, result)
|
|
964
|
+
# Check if result contains an error
|
|
965
|
+
if result.is_a?(Hash) && result['error']
|
|
966
|
+
send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
|
|
967
|
+
return
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
response = {
|
|
971
|
+
'jsonrpc' => '2.0',
|
|
972
|
+
'id' => request_id,
|
|
973
|
+
'result' => result
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
# Send response via HTTP POST
|
|
977
|
+
post_jsonrpc_response(response)
|
|
978
|
+
rescue StandardError => e
|
|
979
|
+
@logger.error("Error sending sampling response: #{e.message}")
|
|
980
|
+
end
|
|
981
|
+
|
|
852
982
|
# Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
|
|
853
983
|
# For streamable HTTP, this is sent as a JSON-RPC request (not response)
|
|
854
984
|
# because HTTP is unidirectional.
|