ruby-mcp-client 0.8.1 → 0.9.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.
@@ -21,7 +21,8 @@ module MCPClient
21
21
  # Initialize a new MCPClient::Client
22
22
  # @param mcp_server_configs [Array<Hash>] configurations for MCP servers
23
23
  # @param logger [Logger, nil] optional logger, defaults to STDOUT
24
- def initialize(mcp_server_configs: [], logger: nil)
24
+ # @param elicitation_handler [Proc, nil] optional handler for elicitation requests (MCP 2025-06-18)
25
+ def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil)
25
26
  @logger = logger || Logger.new($stdout, level: Logger::WARN)
26
27
  @logger.progname = self.class.name
27
28
  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
@@ -34,6 +35,8 @@ module MCPClient
34
35
  @resource_cache = {}
35
36
  # JSON-RPC notification listeners
36
37
  @notification_listeners = []
38
+ # Elicitation handler (MCP 2025-06-18)
39
+ @elicitation_handler = elicitation_handler
37
40
  # Register default and user-defined notification handlers on each server
38
41
  @servers.each do |server|
39
42
  server.on_notification do |method, params|
@@ -42,6 +45,10 @@ module MCPClient
42
45
  # Invoke user-defined listeners
43
46
  @notification_listeners.each { |cb| cb.call(server, method, params) }
44
47
  end
48
+ # Register elicitation handler on each server
49
+ if server.respond_to?(:on_elicitation_request)
50
+ server.on_elicitation_request(&method(:handle_elicitation_request))
51
+ end
45
52
  end
46
53
  end
47
54
 
@@ -598,5 +605,70 @@ module MCPClient
598
605
  "Error reading resource '#{uri}': #{e.message} (Server: #{server_id})"
599
606
  end
600
607
  end
608
+
609
+ # Handle elicitation request from server (MCP 2025-06-18)
610
+ # @param request_id [String, Integer] the JSON-RPC request ID
611
+ # @param params [Hash] the elicitation parameters
612
+ # @return [Hash] the elicitation response
613
+ def handle_elicitation_request(_request_id, params)
614
+ # If no handler is configured, decline the request
615
+ unless @elicitation_handler
616
+ @logger.warn('Received elicitation request but no handler configured, declining')
617
+ return { 'action' => 'decline' }
618
+ end
619
+
620
+ message = params['message']
621
+ schema = params['schema'] || params['requestedSchema']
622
+ metadata = params['metadata']
623
+
624
+ begin
625
+ # Call the user-defined handler
626
+ result = case @elicitation_handler.arity
627
+ when 0
628
+ @elicitation_handler.call
629
+ when 1
630
+ @elicitation_handler.call(message)
631
+ when 2, -1
632
+ @elicitation_handler.call(message, schema)
633
+ else
634
+ @elicitation_handler.call(message, schema, metadata)
635
+ end
636
+
637
+ # Validate and format response
638
+ case result
639
+ when Hash
640
+ if result['action']
641
+ normalised_action_response(result)
642
+ elsif result[:action]
643
+ # Convert symbol keys to strings
644
+ {
645
+ 'action' => result[:action].to_s,
646
+ 'content' => result[:content]
647
+ }.compact.then { |payload| normalised_action_response(payload) }
648
+ else
649
+ # Assume it's content for an accept action
650
+ { 'action' => 'accept', 'content' => result }
651
+ end
652
+ when nil
653
+ { 'action' => 'cancel' }
654
+ else
655
+ { 'action' => 'accept', 'content' => result }
656
+ end
657
+ rescue StandardError => e
658
+ @logger.error("Elicitation handler error: #{e.message}")
659
+ @logger.debug(e.backtrace.join("\n"))
660
+ { 'action' => 'decline' }
661
+ end
662
+ end
663
+
664
+ # Ensure the action value conforms to MCP spec (accept, decline, cancel)
665
+ # Falls back to accept for unknown action values.
666
+ def normalised_action_response(result)
667
+ action = result['action']
668
+ return result if %w[accept decline cancel].include?(action)
669
+
670
+ @logger.warn("Unknown elicitation action '#{action}', defaulting to accept")
671
+ result.merge('action' => 'accept')
672
+ end
601
673
  end
602
674
  end
@@ -64,7 +64,9 @@ module MCPClient
64
64
  def initialization_params
65
65
  {
66
66
  'protocolVersion' => MCPClient::PROTOCOL_VERSION,
67
- 'capabilities' => {},
67
+ 'capabilities' => {
68
+ 'elicitation' => {} # MCP 2025-06-18: Support for server-initiated user interactions
69
+ },
68
70
  'clientInfo' => { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION }
69
71
  }
70
72
  end
@@ -76,7 +76,8 @@ module MCPClient
76
76
  retries: config[:retries] || 3,
77
77
  retry_backoff: config[:retry_backoff] || 1,
78
78
  name: config[:name],
79
- logger: logger
79
+ logger: logger,
80
+ oauth_provider: config[:oauth_provider]
80
81
  )
81
82
  end
82
83
 
@@ -95,7 +96,8 @@ module MCPClient
95
96
  retries: config[:retries] || 3,
96
97
  retry_backoff: config[:retry_backoff] || 1,
97
98
  name: config[:name],
98
- logger: logger
99
+ logger: logger,
100
+ oauth_provider: config[:oauth_provider]
99
101
  )
100
102
  end
101
103
 
@@ -12,6 +12,14 @@ module MCPClient
12
12
  # Implementation of MCP server that communicates via HTTP requests/responses
13
13
  # Useful for communicating with MCP servers that support HTTP-based transport
14
14
  # without Server-Sent Events streaming
15
+ #
16
+ # @note Elicitation Support (MCP 2025-06-18)
17
+ # This transport does NOT support server-initiated elicitation requests.
18
+ # The HTTP transport uses a pure request-response architecture where only the client
19
+ # can initiate requests. For elicitation support, use one of these transports instead:
20
+ # - ServerStdio: Full bidirectional JSON-RPC over stdin/stdout
21
+ # - ServerSSE: Server requests via SSE stream, client responses via HTTP POST
22
+ # - ServerStreamableHTTP: Server requests via SSE-formatted responses, client responses via HTTP POST
15
23
  class ServerHTTP < ServerBase
16
24
  require_relative 'server_http/json_rpc_transport'
17
25
 
@@ -31,6 +31,7 @@ module MCPClient
31
31
  data = JSON.parse(event[:data])
32
32
 
33
33
  return if process_error_in_message(data)
34
+ return if process_server_request?(data)
34
35
  return if process_notification?(data)
35
36
 
36
37
  process_response?(data)
@@ -58,6 +59,16 @@ module MCPClient
58
59
  true
59
60
  end
60
61
 
62
+ # Process a JSON-RPC request from server (has both id AND method)
63
+ # @param data [Hash] the parsed JSON payload
64
+ # @return [Boolean] true if we saw & handled a server request
65
+ def process_server_request?(data)
66
+ return false unless data['method'] && data.key?('id')
67
+
68
+ handle_server_request(data)
69
+ true
70
+ end
71
+
61
72
  # Process a JSON-RPC notification (no id => notification)
62
73
  # @param data [Hash] the parsed JSON payload
63
74
  # @return [Boolean] true if we saw & handled a notification
@@ -11,6 +11,12 @@ require 'faraday/follow_redirects'
11
11
  module MCPClient
12
12
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
13
13
  # Useful for communicating with remote MCP servers over HTTP
14
+ #
15
+ # @note Elicitation Support (MCP 2025-06-18)
16
+ # This transport FULLY supports server-initiated elicitation requests via bidirectional
17
+ # JSON-RPC. The server sends elicitation/create requests via the SSE stream, and the
18
+ # client responds via HTTP POST to the RPC endpoint. This provides full elicitation
19
+ # capability for remote servers.
14
20
  class ServerSSE < ServerBase
15
21
  require_relative 'server_sse/sse_parser'
16
22
  require_relative 'server_sse/json_rpc_transport'
@@ -98,6 +104,7 @@ module MCPClient
98
104
  # Time of last activity
99
105
  @last_activity_time = Time.now
100
106
  @activity_timer_thread = nil
107
+ @elicitation_request_callback = nil # MCP 2025-06-18
101
108
  end
102
109
 
103
110
  # Stream tool call fallback for SSE transport (yields single result)
@@ -409,6 +416,125 @@ module MCPClient
409
416
  end
410
417
  end
411
418
 
419
+ # Register a callback for elicitation requests (MCP 2025-06-18)
420
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
421
+ # @return [void]
422
+ def on_elicitation_request(&block)
423
+ @elicitation_request_callback = block
424
+ end
425
+
426
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
427
+ # @param msg [Hash] the JSON-RPC request message
428
+ # @return [void]
429
+ def handle_server_request(msg)
430
+ request_id = msg['id']
431
+ method = msg['method']
432
+ params = msg['params'] || {}
433
+
434
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
435
+
436
+ case method
437
+ when 'elicitation/create'
438
+ handle_elicitation_create(request_id, params)
439
+ else
440
+ # Unknown request method, send error response
441
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
442
+ end
443
+ rescue StandardError => e
444
+ @logger.error("Error handling server request: #{e.message}")
445
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
446
+ end
447
+
448
+ # Handle elicitation/create request from server (MCP 2025-06-18)
449
+ # @param request_id [String, Integer] the JSON-RPC request ID
450
+ # @param params [Hash] the elicitation parameters
451
+ # @return [void]
452
+ def handle_elicitation_create(request_id, params)
453
+ # If no callback is registered, decline the request
454
+ unless @elicitation_request_callback
455
+ @logger.warn('Received elicitation request but no callback registered, declining')
456
+ send_elicitation_response(request_id, { 'action' => 'decline' })
457
+ return
458
+ end
459
+
460
+ # Call the registered callback
461
+ result = @elicitation_request_callback.call(request_id, params)
462
+
463
+ # Send the response back to the server
464
+ send_elicitation_response(request_id, result)
465
+ end
466
+
467
+ # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
468
+ # @param request_id [String, Integer] the JSON-RPC request ID
469
+ # @param result [Hash] the elicitation result (action and optional content)
470
+ # @return [void]
471
+ def send_elicitation_response(request_id, result)
472
+ ensure_initialized
473
+
474
+ response = {
475
+ 'jsonrpc' => '2.0',
476
+ 'id' => request_id,
477
+ 'result' => result
478
+ }
479
+
480
+ # Send response via HTTP POST to the RPC endpoint
481
+ post_jsonrpc_response(response)
482
+ rescue StandardError => e
483
+ @logger.error("Error sending elicitation response: #{e.message}")
484
+ end
485
+
486
+ # Send error response back to server via HTTP POST (MCP 2025-06-18)
487
+ # @param request_id [String, Integer] the JSON-RPC request ID
488
+ # @param code [Integer] the error code
489
+ # @param message [String] the error message
490
+ # @return [void]
491
+ def send_error_response(request_id, code, message)
492
+ ensure_initialized
493
+
494
+ response = {
495
+ 'jsonrpc' => '2.0',
496
+ 'id' => request_id,
497
+ 'error' => {
498
+ 'code' => code,
499
+ 'message' => message
500
+ }
501
+ }
502
+
503
+ # Send response via HTTP POST to the RPC endpoint
504
+ post_jsonrpc_response(response)
505
+ rescue StandardError => e
506
+ @logger.error("Error sending error response: #{e.message}")
507
+ end
508
+
509
+ # Post a JSON-RPC response message to the server via HTTP
510
+ # @param response [Hash] the JSON-RPC response
511
+ # @return [void]
512
+ # @private
513
+ def post_jsonrpc_response(response)
514
+ unless @rpc_endpoint
515
+ @logger.error('Cannot send response: RPC endpoint not available')
516
+ return
517
+ end
518
+
519
+ # Use the same connection pattern as post_json_rpc_request
520
+ uri = URI.parse(@base_url)
521
+ base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
522
+ @rpc_conn ||= create_json_rpc_connection(base)
523
+
524
+ json_body = JSON.generate(response)
525
+
526
+ @rpc_conn.post do |req|
527
+ req.url @rpc_endpoint
528
+ req.headers['Content-Type'] = 'application/json'
529
+ @headers.each { |k, v| req.headers[k] = v }
530
+ req.body = json_body
531
+ end
532
+
533
+ @logger.debug("Sent response via HTTP POST: #{json_body}")
534
+ rescue StandardError => e
535
+ @logger.error("Failed to send response via HTTP POST: #{e.message}")
536
+ end
537
+
412
538
  private
413
539
 
414
540
  # Start the SSE thread to listen for events
@@ -46,6 +46,7 @@ module MCPClient
46
46
  @retry_backoff = retry_backoff
47
47
  @read_timeout = read_timeout
48
48
  @env = env || {}
49
+ @elicitation_request_callback = nil # MCP 2025-06-18
49
50
  end
50
51
 
51
52
  # Server info from the initialize response
@@ -95,12 +96,20 @@ module MCPClient
95
96
  def handle_line(line)
96
97
  msg = JSON.parse(line)
97
98
  @logger.debug("Received line: #{line.chomp}")
99
+
100
+ # Dispatch JSON-RPC requests from server (has id AND method) - MCP 2025-06-18
101
+ if msg['method'] && msg.key?('id')
102
+ handle_server_request(msg)
103
+ return
104
+ end
105
+
98
106
  # Dispatch JSON-RPC notifications (no id, has method)
99
107
  if msg['method'] && !msg.key?('id')
100
108
  @notification_callback&.call(msg['method'], msg['params'])
101
109
  return
102
110
  end
103
- # Handle standard JSON-RPC responses
111
+
112
+ # Handle standard JSON-RPC responses (has id, no method)
104
113
  id = msg['id']
105
114
  return unless id
106
115
 
@@ -331,6 +340,96 @@ module MCPClient
331
340
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
332
341
  end
333
342
 
343
+ # Register a callback for elicitation requests (MCP 2025-06-18)
344
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
345
+ # @return [void]
346
+ def on_elicitation_request(&block)
347
+ @elicitation_request_callback = block
348
+ end
349
+
350
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
351
+ # @param msg [Hash] the JSON-RPC request message
352
+ # @return [void]
353
+ def handle_server_request(msg)
354
+ request_id = msg['id']
355
+ method = msg['method']
356
+ params = msg['params'] || {}
357
+
358
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
359
+
360
+ case method
361
+ when 'elicitation/create'
362
+ handle_elicitation_create(request_id, params)
363
+ else
364
+ # Unknown request method, send error response
365
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
366
+ end
367
+ rescue StandardError => e
368
+ @logger.error("Error handling server request: #{e.message}")
369
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
370
+ end
371
+
372
+ # Handle elicitation/create request from server (MCP 2025-06-18)
373
+ # @param request_id [String, Integer] the JSON-RPC request ID
374
+ # @param params [Hash] the elicitation parameters
375
+ # @return [void]
376
+ def handle_elicitation_create(request_id, params)
377
+ # If no callback is registered, decline the request
378
+ unless @elicitation_request_callback
379
+ @logger.warn('Received elicitation request but no callback registered, declining')
380
+ send_elicitation_response(request_id, { 'action' => 'decline' })
381
+ return
382
+ end
383
+
384
+ # Call the registered callback
385
+ result = @elicitation_request_callback.call(request_id, params)
386
+
387
+ # Send the response back to the server
388
+ send_elicitation_response(request_id, result)
389
+ end
390
+
391
+ # Send elicitation response back to server (MCP 2025-06-18)
392
+ # @param request_id [String, Integer] the JSON-RPC request ID
393
+ # @param result [Hash] the elicitation result (action and optional content)
394
+ # @return [void]
395
+ def send_elicitation_response(request_id, result)
396
+ response = {
397
+ 'jsonrpc' => '2.0',
398
+ 'id' => request_id,
399
+ 'result' => result
400
+ }
401
+ send_message(response)
402
+ end
403
+
404
+ # Send error response back to server (MCP 2025-06-18)
405
+ # @param request_id [String, Integer] the JSON-RPC request ID
406
+ # @param code [Integer] the error code
407
+ # @param message [String] the error message
408
+ # @return [void]
409
+ def send_error_response(request_id, code, message)
410
+ response = {
411
+ 'jsonrpc' => '2.0',
412
+ 'id' => request_id,
413
+ 'error' => {
414
+ 'code' => code,
415
+ 'message' => message
416
+ }
417
+ }
418
+ send_message(response)
419
+ end
420
+
421
+ # Send a JSON-RPC message to the server
422
+ # @param message [Hash] the message to send
423
+ # @return [void]
424
+ def send_message(message)
425
+ json = JSON.generate(message)
426
+ @stdin.puts(json)
427
+ @stdin.flush
428
+ @logger.debug("Sent message: #{json}")
429
+ rescue StandardError => e
430
+ @logger.error("Error sending message: #{e.message}")
431
+ end
432
+
334
433
  # Clean up the server connection
335
434
  # Closes all stdio handles and terminates any running processes and threads
336
435
  # @return [void]
@@ -2,6 +2,9 @@
2
2
 
3
3
  require_relative '../http_transport_base'
4
4
 
5
+ require 'zlib'
6
+ require 'stringio'
7
+
5
8
  module MCPClient
6
9
  class ServerStreamableHTTP
7
10
  # JSON-RPC request/notification plumbing for Streamable HTTP transport
@@ -23,8 +26,12 @@ module MCPClient
23
26
  # @raise [MCPClient::Errors::TransportError] if parsing fails
24
27
  # @raise [MCPClient::Errors::ServerError] if the response contains an error
25
28
  def parse_response(response)
26
- body = response.body.strip
29
+ body = response.body
27
30
  content_type = response.headers['content-type'] || response.headers['Content-Type'] || ''
31
+ content_encoding = response.headers['content-encoding'] || response.headers['Content-Encoding'] || ''
32
+
33
+ body = Zlib::GzipReader.new(StringIO.new(body)).read if content_encoding.include?('gzip')
34
+ body = body&.strip
28
35
 
29
36
  # Determine response format based on Content-Type header per MCP 2025 spec
30
37
  data = if content_type.include?('text/event-stream')
@@ -9,9 +9,9 @@ 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 (MCP 2025-03-26)
12
+ # Implementation of MCP server that communicates via Streamable HTTP transport (MCP 2025-06-18)
13
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
14
+ # Compliant with MCP specification version 2025-06-18
15
15
  #
16
16
  # Key features:
17
17
  # - Supports server-sent events (SSE) for real-time notifications
@@ -91,7 +91,7 @@ module MCPClient
91
91
  @headers = opts[:headers].merge({
92
92
  'Content-Type' => 'application/json',
93
93
  'Accept' => 'text/event-stream, application/json',
94
- 'Accept-Encoding' => 'gzip, deflate',
94
+ 'Accept-Encoding' => 'gzip',
95
95
  'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}",
96
96
  'Cache-Control' => 'no-cache'
97
97
  })
@@ -116,6 +116,7 @@ module MCPClient
116
116
  @events_connection = nil
117
117
  @events_thread = nil
118
118
  @buffer = '' # Buffer for partial SSE event data
119
+ @elicitation_request_callback = nil # MCP 2025-06-18
119
120
  end
120
121
 
121
122
  # Connect to the MCP server over Streamable HTTP
@@ -451,6 +452,13 @@ module MCPClient
451
452
  end
452
453
  end
453
454
 
455
+ # Register a callback for elicitation requests (MCP 2025-06-18)
456
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
457
+ # @return [void]
458
+ def on_elicitation_request(&block)
459
+ @elicitation_request_callback = block
460
+ end
461
+
454
462
  private
455
463
 
456
464
  def perform_initialize
@@ -755,12 +763,12 @@ module MCPClient
755
763
  # Handle ping requests from server (keepalive mechanism)
756
764
  if message['method'] == 'ping' && message.key?('id')
757
765
  handle_ping_request(message['id'])
766
+ elsif message['method'] && message.key?('id')
767
+ # Handle server-to-client requests (MCP 2025-06-18)
768
+ handle_server_request(message)
758
769
  elsif message['method'] && !message.key?('id')
759
770
  # Handle server notifications (messages without id)
760
771
  @notification_callback&.call(message['method'], message['params'])
761
- elsif message.key?('id')
762
- # This might be a server-to-client request (future MCP versions)
763
- @logger.warn("Received unhandled server request: #{message['method']}")
764
772
  end
765
773
  rescue JSON::ParserError => e
766
774
  @logger.error("Invalid JSON in server message: #{e.message}")
@@ -796,5 +804,123 @@ module MCPClient
796
804
  @logger.error("Failed to send pong response: #{e.message}")
797
805
  end
798
806
  end
807
+
808
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
809
+ # @param msg [Hash] the JSON-RPC request message
810
+ # @return [void]
811
+ def handle_server_request(msg)
812
+ request_id = msg['id']
813
+ method = msg['method']
814
+ params = msg['params'] || {}
815
+
816
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
817
+
818
+ case method
819
+ when 'elicitation/create'
820
+ handle_elicitation_create(request_id, params)
821
+ else
822
+ # Unknown request method, send error response
823
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
824
+ end
825
+ rescue StandardError => e
826
+ @logger.error("Error handling server request: #{e.message}")
827
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
828
+ end
829
+
830
+ # Handle elicitation/create request from server (MCP 2025-06-18)
831
+ # @param request_id [String, Integer] the JSON-RPC request ID (used as elicitationId)
832
+ # @param params [Hash] the elicitation parameters
833
+ # @return [void]
834
+ def handle_elicitation_create(request_id, params)
835
+ # The request_id is the elicitationId per MCP spec
836
+ elicitation_id = request_id
837
+
838
+ # If no callback is registered, decline the request
839
+ unless @elicitation_request_callback
840
+ @logger.warn('Received elicitation request but no callback registered, declining')
841
+ send_elicitation_response(elicitation_id, { 'action' => 'decline' })
842
+ return
843
+ end
844
+
845
+ # Call the registered callback
846
+ result = @elicitation_request_callback.call(request_id, params)
847
+
848
+ # Send the response back to the server
849
+ send_elicitation_response(elicitation_id, result)
850
+ end
851
+
852
+ # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
853
+ # For streamable HTTP, this is sent as a JSON-RPC request (not response)
854
+ # because HTTP is unidirectional.
855
+ # @param elicitation_id [String] the elicitation ID from the server
856
+ # @param result [Hash] the elicitation result (action and optional content)
857
+ # @return [void]
858
+ def send_elicitation_response(elicitation_id, result)
859
+ params = {
860
+ 'elicitationId' => elicitation_id,
861
+ 'action' => result['action']
862
+ }
863
+
864
+ # Only include content if present (typically for 'accept' action)
865
+ params['content'] = result['content'] if result['content']
866
+
867
+ request = {
868
+ 'jsonrpc' => '2.0',
869
+ 'method' => 'elicitation/response',
870
+ 'params' => params
871
+ }
872
+
873
+ # Send as a JSON-RPC request via HTTP POST
874
+ post_jsonrpc_response(request)
875
+ rescue StandardError => e
876
+ @logger.error("Error sending elicitation response: #{e.message}")
877
+ end
878
+
879
+ # Send error response back to server via HTTP POST (MCP 2025-06-18)
880
+ # @param request_id [String, Integer] the JSON-RPC request ID
881
+ # @param code [Integer] the error code
882
+ # @param message [String] the error message
883
+ # @return [void]
884
+ def send_error_response(request_id, code, message)
885
+ response = {
886
+ 'jsonrpc' => '2.0',
887
+ 'id' => request_id,
888
+ 'error' => {
889
+ 'code' => code,
890
+ 'message' => message
891
+ }
892
+ }
893
+
894
+ # Send response via HTTP POST to the endpoint
895
+ post_jsonrpc_response(response)
896
+ rescue StandardError => e
897
+ @logger.error("Error sending error response: #{e.message}")
898
+ end
899
+
900
+ # Post a JSON-RPC response message to the server via HTTP
901
+ # @param response [Hash] the JSON-RPC response
902
+ # @return [void]
903
+ # @private
904
+ def post_jsonrpc_response(response)
905
+ # Send response in a separate thread to avoid blocking event processing
906
+ Thread.new do
907
+ conn = http_connection
908
+ json_body = JSON.generate(response)
909
+
910
+ resp = conn.post(@endpoint) do |req|
911
+ @headers.each { |k, v| req.headers[k] = v }
912
+ req.headers['Mcp-Session-Id'] = @session_id if @session_id
913
+ req.body = json_body
914
+ end
915
+
916
+ if resp.success?
917
+ @logger.debug("Sent JSON-RPC response: #{json_body}")
918
+ else
919
+ @logger.warn("Failed to send JSON-RPC response: HTTP #{resp.status}")
920
+ end
921
+ rescue StandardError => e
922
+ @logger.error("Failed to send JSON-RPC response: #{e.message}")
923
+ end
924
+ end
799
925
  end
800
926
  end