ruby-mcp-client 0.9.0 → 1.0.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.
@@ -105,6 +105,8 @@ module MCPClient
105
105
  @last_activity_time = Time.now
106
106
  @activity_timer_thread = nil
107
107
  @elicitation_request_callback = nil # MCP 2025-06-18
108
+ @roots_list_request_callback = nil # MCP 2025-06-18
109
+ @sampling_request_callback = nil # MCP 2025-11-25
108
110
  end
109
111
 
110
112
  # Stream tool call fallback for SSE transport (yields single result)
@@ -307,15 +309,11 @@ module MCPClient
307
309
  # Call a tool with the given parameters
308
310
  # @param tool_name [String] the name of the tool to call
309
311
  # @param parameters [Hash] the parameters to pass to the tool
310
- # @return [Object] the result of the tool invocation
312
+ # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
311
313
  # @raise [MCPClient::Errors::ServerError] if server returns an error
312
314
  # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
313
315
  # @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
314
316
  # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
315
- # Call a tool with the given parameters
316
- # @param tool_name [String] the name of the tool to call
317
- # @param parameters [Hash] the parameters to pass to the tool
318
- # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
319
317
  def call_tool(tool_name, parameters)
320
318
  rpc_request('tools/call', {
321
319
  name: tool_name,
@@ -329,6 +327,36 @@ module MCPClient
329
327
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
330
328
  end
331
329
 
330
+ # Request completion suggestions from the server (MCP 2025-06-18)
331
+ # @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
332
+ # @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
333
+ # @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
334
+ # @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
335
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
336
+ def complete(ref:, argument:, context: nil)
337
+ params = { ref: ref, argument: argument }
338
+ params[:context] = context if context
339
+ result = rpc_request('completion/complete', params)
340
+ result['completion'] || { 'values' => [] }
341
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
342
+ raise
343
+ rescue StandardError => e
344
+ raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
345
+ end
346
+
347
+ # Set the logging level on the server (MCP 2025-06-18)
348
+ # @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
349
+ # 'critical', 'alert', 'emergency')
350
+ # @return [Hash] empty result on success
351
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
352
+ def log_level=(level)
353
+ rpc_request('logging/setLevel', { level: level })
354
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
355
+ raise
356
+ rescue StandardError => e
357
+ raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
358
+ end
359
+
332
360
  # Connect to the MCP server over HTTP/HTTPS with SSE
333
361
  # @return [Boolean] true if connection was successful
334
362
  # @raise [MCPClient::Errors::ConnectionError] if connection fails
@@ -423,6 +451,20 @@ module MCPClient
423
451
  @elicitation_request_callback = block
424
452
  end
425
453
 
454
+ # Register a callback for roots/list requests (MCP 2025-06-18)
455
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
456
+ # @return [void]
457
+ def on_roots_list_request(&block)
458
+ @roots_list_request_callback = block
459
+ end
460
+
461
+ # Register a callback for sampling requests (MCP 2025-11-25)
462
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
463
+ # @return [void]
464
+ def on_sampling_request(&block)
465
+ @sampling_request_callback = block
466
+ end
467
+
426
468
  # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
427
469
  # @param msg [Hash] the JSON-RPC request message
428
470
  # @return [void]
@@ -436,6 +478,10 @@ module MCPClient
436
478
  case method
437
479
  when 'elicitation/create'
438
480
  handle_elicitation_create(request_id, params)
481
+ when 'roots/list'
482
+ handle_roots_list(request_id, params)
483
+ when 'sampling/createMessage'
484
+ handle_sampling_create_message(request_id, params)
439
485
  else
440
486
  # Unknown request method, send error response
441
487
  send_error_response(request_id, -32_601, "Method not found: #{method}")
@@ -464,6 +510,88 @@ module MCPClient
464
510
  send_elicitation_response(request_id, result)
465
511
  end
466
512
 
513
+ # Handle roots/list request from server (MCP 2025-06-18)
514
+ # @param request_id [String, Integer] the JSON-RPC request ID
515
+ # @param params [Hash] the request parameters
516
+ # @return [void]
517
+ def handle_roots_list(request_id, params)
518
+ # If no callback is registered, return empty roots list
519
+ unless @roots_list_request_callback
520
+ @logger.debug('Received roots/list request but no callback registered, returning empty list')
521
+ send_roots_list_response(request_id, { 'roots' => [] })
522
+ return
523
+ end
524
+
525
+ # Call the registered callback
526
+ result = @roots_list_request_callback.call(request_id, params)
527
+
528
+ # Send the response back to the server
529
+ send_roots_list_response(request_id, result)
530
+ end
531
+
532
+ # Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
533
+ # @param request_id [String, Integer] the JSON-RPC request ID
534
+ # @param result [Hash] the roots list result
535
+ # @return [void]
536
+ def send_roots_list_response(request_id, result)
537
+ ensure_initialized
538
+
539
+ response = {
540
+ 'jsonrpc' => '2.0',
541
+ 'id' => request_id,
542
+ 'result' => result
543
+ }
544
+
545
+ # Send response via HTTP POST to the RPC endpoint
546
+ post_jsonrpc_response(response)
547
+ rescue StandardError => e
548
+ @logger.error("Error sending roots/list response: #{e.message}")
549
+ end
550
+
551
+ # Handle sampling/createMessage request from server (MCP 2025-11-25)
552
+ # @param request_id [String, Integer] the JSON-RPC request ID
553
+ # @param params [Hash] the sampling parameters
554
+ # @return [void]
555
+ def handle_sampling_create_message(request_id, params)
556
+ # If no callback is registered, return error
557
+ unless @sampling_request_callback
558
+ @logger.warn('Received sampling request but no callback registered, returning error')
559
+ send_error_response(request_id, -1, 'Sampling not supported')
560
+ return
561
+ end
562
+
563
+ # Call the registered callback
564
+ result = @sampling_request_callback.call(request_id, params)
565
+
566
+ # Send the response back to the server
567
+ send_sampling_response(request_id, result)
568
+ end
569
+
570
+ # Send sampling response back to server via HTTP POST (MCP 2025-11-25)
571
+ # @param request_id [String, Integer] the JSON-RPC request ID
572
+ # @param result [Hash] the sampling result (role, content, model, stopReason)
573
+ # @return [void]
574
+ def send_sampling_response(request_id, result)
575
+ ensure_initialized
576
+
577
+ # Check if result contains an error
578
+ if result.is_a?(Hash) && result['error']
579
+ send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
580
+ return
581
+ end
582
+
583
+ response = {
584
+ 'jsonrpc' => '2.0',
585
+ 'id' => request_id,
586
+ 'result' => result
587
+ }
588
+
589
+ # Send response via HTTP POST to the RPC endpoint
590
+ post_jsonrpc_response(response)
591
+ rescue StandardError => e
592
+ @logger.error("Error sending sampling response: #{e.message}")
593
+ end
594
+
467
595
  # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
468
596
  # @param request_id [String, Integer] the JSON-RPC request ID
469
597
  # @param result [Hash] the elicitation result (action and optional content)
@@ -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-11-25
50
52
  end
51
53
 
52
54
  # Server info from the initialize response
@@ -340,6 +342,59 @@ 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
+ # @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
349
+ # @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
350
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
351
+ def complete(ref:, argument:, context: nil)
352
+ ensure_initialized
353
+ req_id = next_id
354
+ params = { 'ref' => ref, 'argument' => argument }
355
+ params['context'] = context if context
356
+ req = {
357
+ 'jsonrpc' => '2.0',
358
+ 'id' => req_id,
359
+ 'method' => 'completion/complete',
360
+ 'params' => params
361
+ }
362
+ send_request(req)
363
+ res = wait_response(req_id)
364
+ if (err = res['error'])
365
+ raise MCPClient::Errors::ServerError, err['message']
366
+ end
367
+
368
+ res.dig('result', 'completion') || { 'values' => [] }
369
+ rescue StandardError => e
370
+ raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
371
+ end
372
+
373
+ # Set the logging level on the server (MCP 2025-06-18)
374
+ # @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
375
+ # 'critical', 'alert', 'emergency')
376
+ # @return [Hash] empty result on success
377
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
378
+ def log_level=(level)
379
+ ensure_initialized
380
+ req_id = next_id
381
+ req = {
382
+ 'jsonrpc' => '2.0',
383
+ 'id' => req_id,
384
+ 'method' => 'logging/setLevel',
385
+ 'params' => { 'level' => level }
386
+ }
387
+ send_request(req)
388
+ res = wait_response(req_id)
389
+ if (err = res['error'])
390
+ raise MCPClient::Errors::ServerError, err['message']
391
+ end
392
+
393
+ res['result'] || {}
394
+ rescue StandardError => e
395
+ raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
396
+ end
397
+
343
398
  # Register a callback for elicitation requests (MCP 2025-06-18)
344
399
  # @param block [Proc] callback that receives (request_id, params) and returns response hash
345
400
  # @return [void]
@@ -347,6 +402,20 @@ module MCPClient
347
402
  @elicitation_request_callback = block
348
403
  end
349
404
 
405
+ # Register a callback for roots/list requests (MCP 2025-06-18)
406
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
407
+ # @return [void]
408
+ def on_roots_list_request(&block)
409
+ @roots_list_request_callback = block
410
+ end
411
+
412
+ # Register a callback for sampling requests (MCP 2025-11-25)
413
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
414
+ # @return [void]
415
+ def on_sampling_request(&block)
416
+ @sampling_request_callback = block
417
+ end
418
+
350
419
  # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
351
420
  # @param msg [Hash] the JSON-RPC request message
352
421
  # @return [void]
@@ -360,6 +429,10 @@ module MCPClient
360
429
  case method
361
430
  when 'elicitation/create'
362
431
  handle_elicitation_create(request_id, params)
432
+ when 'roots/list'
433
+ handle_roots_list(request_id, params)
434
+ when 'sampling/createMessage'
435
+ handle_sampling_create_message(request_id, params)
363
436
  else
364
437
  # Unknown request method, send error response
365
438
  send_error_response(request_id, -32_601, "Method not found: #{method}")
@@ -388,6 +461,76 @@ module MCPClient
388
461
  send_elicitation_response(request_id, result)
389
462
  end
390
463
 
464
+ # Handle roots/list request from server (MCP 2025-06-18)
465
+ # @param request_id [String, Integer] the JSON-RPC request ID
466
+ # @param params [Hash] the request parameters
467
+ # @return [void]
468
+ def handle_roots_list(request_id, params)
469
+ # If no callback is registered, return empty roots list
470
+ unless @roots_list_request_callback
471
+ @logger.debug('Received roots/list request but no callback registered, returning empty list')
472
+ send_roots_list_response(request_id, { 'roots' => [] })
473
+ return
474
+ end
475
+
476
+ # Call the registered callback
477
+ result = @roots_list_request_callback.call(request_id, params)
478
+
479
+ # Send the response back to the server
480
+ send_roots_list_response(request_id, result)
481
+ end
482
+
483
+ # Handle sampling/createMessage request from server (MCP 2025-11-25)
484
+ # @param request_id [String, Integer] the JSON-RPC request ID
485
+ # @param params [Hash] the sampling parameters
486
+ # @return [void]
487
+ def handle_sampling_create_message(request_id, params)
488
+ # If no callback is registered, return error
489
+ unless @sampling_request_callback
490
+ @logger.warn('Received sampling request but no callback registered, returning error')
491
+ send_error_response(request_id, -1, 'Sampling not supported')
492
+ return
493
+ end
494
+
495
+ # Call the registered callback
496
+ result = @sampling_request_callback.call(request_id, params)
497
+
498
+ # Send the response back to the server
499
+ send_sampling_response(request_id, result)
500
+ end
501
+
502
+ # Send roots/list response back to server (MCP 2025-06-18)
503
+ # @param request_id [String, Integer] the JSON-RPC request ID
504
+ # @param result [Hash] the roots list result
505
+ # @return [void]
506
+ def send_roots_list_response(request_id, result)
507
+ response = {
508
+ 'jsonrpc' => '2.0',
509
+ 'id' => request_id,
510
+ 'result' => result
511
+ }
512
+ send_message(response)
513
+ end
514
+
515
+ # Send sampling response back to server (MCP 2025-11-25)
516
+ # @param request_id [String, Integer] the JSON-RPC request ID
517
+ # @param result [Hash] the sampling result (role, content, model, stopReason)
518
+ # @return [void]
519
+ def send_sampling_response(request_id, result)
520
+ # Check if result contains an error
521
+ if result.is_a?(Hash) && result['error']
522
+ send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
523
+ return
524
+ end
525
+
526
+ response = {
527
+ 'jsonrpc' => '2.0',
528
+ 'id' => request_id,
529
+ 'result' => result
530
+ }
531
+ send_message(response)
532
+ end
533
+
391
534
  # Send elicitation response back to server (MCP 2025-06-18)
392
535
  # @param request_id [String, Integer] the JSON-RPC request ID
393
536
  # @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-11-25
120
123
  end
121
124
 
122
125
  # Connect to the MCP server over Streamable HTTP
@@ -216,6 +219,36 @@ 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
+ # @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
226
+ # @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
227
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
228
+ def complete(ref:, argument:, context: nil)
229
+ params = { ref: ref, argument: argument }
230
+ params[:context] = context if context
231
+ result = rpc_request('completion/complete', params)
232
+ result['completion'] || { 'values' => [] }
233
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
234
+ raise
235
+ rescue StandardError => e
236
+ raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
237
+ end
238
+
239
+ # Set the logging level on the server (MCP 2025-06-18)
240
+ # @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
241
+ # 'critical', 'alert', 'emergency')
242
+ # @return [Hash] empty result on success
243
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
244
+ def log_level=(level)
245
+ rpc_request('logging/setLevel', { level: level })
246
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
247
+ raise
248
+ rescue StandardError => e
249
+ raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
250
+ end
251
+
219
252
  # List all prompts available from the MCP server
220
253
  # @return [Array<MCPClient::Prompt>] list of available prompts
221
254
  # @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
@@ -363,10 +396,17 @@ module MCPClient
363
396
  def apply_request_headers(req, request)
364
397
  super
365
398
 
366
- # Add session header if we have one (for non-initialize requests)
367
- if @session_id && request['method'] != 'initialize'
368
- req.headers['Mcp-Session-Id'] = @session_id
369
- @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
399
+ # Add session and protocol version headers for non-initialize requests
400
+ if request['method'] != 'initialize'
401
+ if @session_id
402
+ req.headers['Mcp-Session-Id'] = @session_id
403
+ @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
404
+ end
405
+
406
+ if @protocol_version
407
+ req.headers['Mcp-Protocol-Version'] = @protocol_version
408
+ @logger.debug("Adding protocol version header: Mcp-Protocol-Version: #{@protocol_version}")
409
+ end
370
410
  end
371
411
 
372
412
  # Add Last-Event-ID header for resumability (if available)
@@ -459,6 +499,20 @@ module MCPClient
459
499
  @elicitation_request_callback = block
460
500
  end
461
501
 
502
+ # Register a callback for roots/list requests (MCP 2025-06-18)
503
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
504
+ # @return [void]
505
+ def on_roots_list_request(&block)
506
+ @roots_list_request_callback = block
507
+ end
508
+
509
+ # Register a callback for sampling requests (MCP 2025-11-25)
510
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
511
+ # @return [void]
512
+ def on_sampling_request(&block)
513
+ @sampling_request_callback = block
514
+ end
515
+
462
516
  private
463
517
 
464
518
  def perform_initialize
@@ -483,7 +537,8 @@ module MCPClient
483
537
  retry_backoff: 1,
484
538
  name: nil,
485
539
  logger: nil,
486
- oauth_provider: nil
540
+ oauth_provider: nil,
541
+ faraday_config: nil
487
542
  }
488
543
  end
489
544
 
@@ -629,6 +684,9 @@ module MCPClient
629
684
  end
630
685
  end
631
686
 
687
+ # Apply user's Faraday customizations after defaults
688
+ @faraday_config&.call(conn)
689
+
632
690
  @logger.debug("Establishing SSE events connection to #{@endpoint}") if @logger.level <= Logger::DEBUG
633
691
 
634
692
  response = conn.get(@endpoint) do |req|
@@ -682,6 +740,7 @@ module MCPClient
682
740
  def apply_events_headers(req)
683
741
  @headers.each { |k, v| req.headers[k] = v }
684
742
  req.headers['Mcp-Session-Id'] = @session_id if @session_id
743
+ req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
685
744
  end
686
745
 
687
746
  # Process event chunks from the server
@@ -792,6 +851,7 @@ module MCPClient
792
851
  response = conn.post(@endpoint) do |req|
793
852
  @headers.each { |k, v| req.headers[k] = v }
794
853
  req.headers['Mcp-Session-Id'] = @session_id if @session_id
854
+ req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
795
855
  req.body = pong_response.to_json
796
856
  end
797
857
 
@@ -818,6 +878,10 @@ module MCPClient
818
878
  case method
819
879
  when 'elicitation/create'
820
880
  handle_elicitation_create(request_id, params)
881
+ when 'roots/list'
882
+ handle_roots_list(request_id, params)
883
+ when 'sampling/createMessage'
884
+ handle_sampling_create_message(request_id, params)
821
885
  else
822
886
  # Unknown request method, send error response
823
887
  send_error_response(request_id, -32_601, "Method not found: #{method}")
@@ -849,6 +913,84 @@ module MCPClient
849
913
  send_elicitation_response(elicitation_id, result)
850
914
  end
851
915
 
916
+ # Handle roots/list request from server (MCP 2025-06-18)
917
+ # @param request_id [String, Integer] the JSON-RPC request ID
918
+ # @param params [Hash] the request parameters
919
+ # @return [void]
920
+ def handle_roots_list(request_id, params)
921
+ # If no callback is registered, return empty roots list
922
+ unless @roots_list_request_callback
923
+ @logger.debug('Received roots/list request but no callback registered, returning empty list')
924
+ send_roots_list_response(request_id, { 'roots' => [] })
925
+ return
926
+ end
927
+
928
+ # Call the registered callback
929
+ result = @roots_list_request_callback.call(request_id, params)
930
+
931
+ # Send the response back to the server
932
+ send_roots_list_response(request_id, result)
933
+ end
934
+
935
+ # Handle sampling/createMessage request from server (MCP 2025-11-25)
936
+ # @param request_id [String, Integer] the JSON-RPC request ID
937
+ # @param params [Hash] the sampling parameters
938
+ # @return [void]
939
+ def handle_sampling_create_message(request_id, params)
940
+ # If no callback is registered, return error
941
+ unless @sampling_request_callback
942
+ @logger.warn('Received sampling request but no callback registered, returning error')
943
+ send_error_response(request_id, -1, 'Sampling not supported')
944
+ return
945
+ end
946
+
947
+ # Call the registered callback
948
+ result = @sampling_request_callback.call(request_id, params)
949
+
950
+ # Send the response back to the server
951
+ send_sampling_response(request_id, result)
952
+ end
953
+
954
+ # Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
955
+ # @param request_id [String, Integer] the JSON-RPC request ID
956
+ # @param result [Hash] the roots list result
957
+ # @return [void]
958
+ def send_roots_list_response(request_id, result)
959
+ response = {
960
+ 'jsonrpc' => '2.0',
961
+ 'id' => request_id,
962
+ 'result' => result
963
+ }
964
+
965
+ # Send response via HTTP POST
966
+ post_jsonrpc_response(response)
967
+ rescue StandardError => e
968
+ @logger.error("Error sending roots/list response: #{e.message}")
969
+ end
970
+
971
+ # Send sampling response back to server via HTTP POST (MCP 2025-11-25)
972
+ # @param request_id [String, Integer] the JSON-RPC request ID
973
+ # @param result [Hash] the sampling result (role, content, model, stopReason)
974
+ # @return [void]
975
+ def send_sampling_response(request_id, result)
976
+ # Check if result contains an error
977
+ if result.is_a?(Hash) && result['error']
978
+ send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
979
+ return
980
+ end
981
+
982
+ response = {
983
+ 'jsonrpc' => '2.0',
984
+ 'id' => request_id,
985
+ 'result' => result
986
+ }
987
+
988
+ # Send response via HTTP POST
989
+ post_jsonrpc_response(response)
990
+ rescue StandardError => e
991
+ @logger.error("Error sending sampling response: #{e.message}")
992
+ end
993
+
852
994
  # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
853
995
  # For streamable HTTP, this is sent as a JSON-RPC request (not response)
854
996
  # because HTTP is unidirectional.
@@ -910,6 +1052,7 @@ module MCPClient
910
1052
  resp = conn.post(@endpoint) do |req|
911
1053
  @headers.each { |k, v| req.headers[k] = v }
912
1054
  req.headers['Mcp-Session-Id'] = @session_id if @session_id
1055
+ req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
913
1056
  req.body = json_body
914
1057
  end
915
1058