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.
@@ -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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.9.0'
5
+ VERSION = '0.9.1'
6
6
 
7
7
  # MCP protocol version (date-based) - unified across all transports
8
8
  PROTOCOL_VERSION = '2025-06-18'