Class: MCPClient::ServerSSE

Inherits:
ServerBase show all
Includes:
JsonRpcTransport, ReconnectMonitor, SseParser
Defined in:
lib/mcp_client/server_sse.rb,
lib/mcp_client/server_sse/sse_parser.rb,
lib/mcp_client/server_sse/reconnect_monitor.rb,
lib/mcp_client/server_sse/json_rpc_transport.rb

Overview

Note:

Elicitation Support (MCP 2025-06-18) This transport FULLY supports server-initiated elicitation requests via bidirectional JSON-RPC. The server sends elicitation/create requests via the SSE stream, and the client responds via HTTP POST to the RPC endpoint. This provides full elicitation capability for remote servers.

Implementation of MCP server that communicates via Server-Sent Events (SSE) Useful for communicating with remote MCP servers over HTTP

Defined Under Namespace

Modules: JsonRpcTransport, ReconnectMonitor, SseParser

Constant Summary collapse

CLOSE_AFTER_PING_RATIO =

Ratio of close_after timeout to ping interval

2.5
DEFAULT_MAX_PING_FAILURES =

Default values for connection monitoring

3
DEFAULT_MAX_RECONNECT_ATTEMPTS =
5
BASE_RECONNECT_DELAY =

Reconnection backoff constants

0.5
MAX_RECONNECT_DELAY =
30
JITTER_FACTOR =
0.25

Constants inherited from ServerBase

MCPClient::ServerBase::MAX_LIST_PAGES, MCPClient::ServerBase::RELATED_TASK_META_KEY

Instance Attribute Summary collapse

Attributes inherited from ServerBase

#instructions, #name

Instance Method Summary collapse

Methods included from ReconnectMonitor

#activity_monitor_loop, #attempt_ping, #attempt_reconnection, #connection_active?, #handle_ping_failure, #handle_sse_auth_error, #record_activity, #reset_connection_state, #setup_sse_connection, #start_activity_monitor, #wait_for_connection

Methods included from JsonRpcTransport

#rpc_notify, #rpc_request, #send_cancellation_notification

Methods included from JsonRpcCommon

#build_jsonrpc_notification, #build_jsonrpc_request, #build_named_request_params, #cancellable_request?, #client_capabilities, #client_info_payload, #declare_sampling_tools, #initialization_params, #ping, #process_jsonrpc_response, #registered_callback?, #sampling_tools_supported?, #split_request_meta, #validate_protocol_version!, #with_retry

Methods included from SseParser

#handle_endpoint_event, #handle_message_event, #parse_and_handle_sse_event, #parse_sse_event, #process_error_in_message?, #process_notification?, #process_response?, #process_server_request?, #resolve_endpoint_uri

Methods inherited from ServerBase

#capability?, #client_info=, #merge_related_task_meta, #on_notification, #ping, #require_capability!, #rpc_notify, #rpc_request

Constructor Details

#initialize(base_url:, headers: {}, read_timeout: 30, ping: 10, retries: 0, retry_backoff: 1, name: nil, logger: nil) ⇒ ServerSSE

Returns a new instance of ServerSSE.

Parameters:

  • base_url (String)

    The base URL of the MCP server

  • headers (Hash) (defaults to: {})

    Additional headers to include in requests

  • read_timeout (Integer) (defaults to: 30)

    Read timeout in seconds (default: 30)

  • ping (Integer) (defaults to: 10)

    Time in seconds after which to send ping if no activity (default: 10)

  • retries (Integer) (defaults to: 0)

    number of retry attempts on transient errors

  • retry_backoff (Numeric) (defaults to: 1)

    base delay in seconds for exponential backoff

  • name (String, nil) (defaults to: nil)

    optional name for this server

  • logger (Logger, nil) (defaults to: nil)

    optional logger



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/mcp_client/server_sse.rb', line 69

def initialize(base_url:, headers: {}, read_timeout: 30, ping: 10,
               retries: 0, retry_backoff: 1, name: nil, logger: nil)
  super(name: name)
  initialize_logger(logger)
  @max_retries = retries
  @retry_backoff = retry_backoff
  # Normalize base_url: preserve trailing slash if explicitly provided for SSE endpoints
  @base_url = base_url
  @headers = headers.merge({
                             'Accept' => 'text/event-stream',
                             'Cache-Control' => 'no-cache',
                             'Connection' => 'keep-alive'
                           })
  # HTTP client is managed via Faraday
  @tools = nil
  @read_timeout = read_timeout
  @ping_interval = ping
  # Set close_after to a multiple of the ping interval
  @close_after = (ping * CLOSE_AFTER_PING_RATIO).to_i

  # SSE-provided JSON-RPC endpoint path for POST requests
  @rpc_endpoint = nil
  @tools_data = nil
  @request_id = 0
  @sse_results = {}
  @mutex = Monitor.new
  @buffer = ''
  @sse_connected = false
  @connection_established = false
  @connection_cv = @mutex.new_cond
  @initialized = false
  # Negotiated protocol version captured from the initialize result
  # (sent as the MCP-Protocol-Version header on post-initialize requests)
  @protocol_version = nil
  @auth_error = nil
  # Non-auth connection failure cause (e.g. invalid endpoint event URI)
  # recorded by the SSE worker for wait_for_connection to surface
  @connection_error = nil
  # Whether to use SSE transport; may disable if handshake fails
  @use_sse = true

  # Time of last activity
  @last_activity_time = Time.now
  @activity_timer_thread = nil
  @elicitation_request_callback = nil # MCP 2025-06-18
  @roots_list_request_callback = nil # MCP 2025-06-18
  @sampling_request_callback = nil # MCP 2025-11-25
end

Instance Attribute Details

#base_urlString (readonly)

Returns The base URL of the MCP server.

Returns:

  • (String)

    The base URL of the MCP server



51
52
53
# File 'lib/mcp_client/server_sse.rb', line 51

def base_url
  @base_url
end

#capabilitiesHash? (readonly)

Server capabilities from initialize response

Returns:

  • (Hash, nil)

    Server capabilities



59
60
61
# File 'lib/mcp_client/server_sse.rb', line 59

def capabilities
  @capabilities
end

#promptsArray<MCPClient::Prompt>? (readonly)

Returns List of available prompts (nil if not fetched yet).

Returns:

  • (Array<MCPClient::Prompt>, nil)

    List of available prompts (nil if not fetched yet)



51
# File 'lib/mcp_client/server_sse.rb', line 51

attr_reader :base_url, :tools, :prompts, :resources

#resourcesObject (readonly)

Returns the value of attribute resources.



51
# File 'lib/mcp_client/server_sse.rb', line 51

attr_reader :base_url, :tools, :prompts, :resources

#server_infoHash? (readonly)

Server information from initialize response

Returns:

  • (Hash, nil)

    Server information



55
56
57
# File 'lib/mcp_client/server_sse.rb', line 55

def server_info
  @server_info
end

#toolsArray<MCPClient::Tool>? (readonly)

Returns List of available tools (nil if not fetched yet).

Returns:

  • (Array<MCPClient::Tool>, nil)

    List of available tools (nil if not fetched yet)



51
# File 'lib/mcp_client/server_sse.rb', line 51

attr_reader :base_url, :tools, :prompts, :resources

Instance Method Details

#call_tool(tool_name, parameters) ⇒ Object

Call a tool with the given parameters

Parameters:

  • tool_name (String)

    the name of the tool to call

  • parameters (Hash)

    the parameters to pass to the tool

Returns:

  • (Object)

    the result of the tool invocation (with string keys for backward compatibility)

Raises:



324
325
326
327
328
329
330
331
332
# File 'lib/mcp_client/server_sse.rb', line 324

def call_tool(tool_name, parameters)
  rpc_request('tools/call', build_named_request_params(tool_name, parameters))
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
  # Re-raise connection/transport errors directly to match test expectations
  raise
rescue StandardError => e
  # For all other errors, wrap in ToolCallError
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
end

#call_tool_streaming(tool_name, parameters) ⇒ Enumerator

Stream tool call fallback for SSE transport (yields single result)

Parameters:

  • tool_name (String)
  • parameters (Hash)

Returns:

  • (Enumerator)


122
123
124
125
126
# File 'lib/mcp_client/server_sse.rb', line 122

def call_tool_streaming(tool_name, parameters)
  Enumerator.new do |yielder|
    yielder << call_tool(tool_name, parameters)
  end
end

#cleanupObject

Note:

This method preserves ping failure and reconnection metrics between reconnection attempts, allowing the client to track failures across multiple connection attempts. This is essential for proper reconnection logic and exponential backoff.

Clean up the server connection Properly closes HTTP connections and clears cached state



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/mcp_client/server_sse.rb', line 412

def cleanup
  @mutex.synchronize do
    # Set flags first before killing threads to prevent race conditions
    # where threads might check flags after they're set but before they're killed
    @connection_established = false
    @sse_connected = false
    @initialized = false # Reset initialization state for reconnection
    # A fresh session negotiates its own protocol version; keeping the old
    # one would leak the previous session's version into the next
    # initialize POST's MCP-Protocol-Version header.
    @protocol_version = nil

    # Reset the SSE parse buffer so a reconnect never inherits a leftover
    # partial event from the previous connection.
    @buffer = ''

    # Log cleanup for debugging
    @logger.debug('Cleaning up SSE connection')

    # Store threads locally to avoid race conditions
    sse_thread = @sse_thread

    # The activity monitor drives reconnection, and reconnection calls this
    # method FROM that thread. Killing the current thread here would abort
    # the reconnect before connect() runs (the historical dead-code bug), so
    # never kill/clear the activity thread when we are running on it. Leaving
    # @activity_timer_thread referenced also makes start_activity_monitor
    # short-circuit, preventing a duplicate monitor after reconnect.
    activity_thread = @activity_timer_thread
    kill_activity_thread = activity_thread && activity_thread != Thread.current

    # Clear thread references first
    @sse_thread = nil
    @activity_timer_thread = nil if kill_activity_thread

    # Kill threads
    begin
      sse_thread&.kill
    rescue StandardError => e
      @logger.debug("Error killing SSE thread: #{e.message}")
    end

    if kill_activity_thread
      begin
        activity_thread.kill
      rescue StandardError => e
        @logger.debug("Error killing activity thread: #{e.message}")
      end
    end

    if @http_client
      @http_client.finish if @http_client.started?
      @http_client = nil
    end

    # Close Faraday connections if they exist
    @rpc_conn = nil
    @sse_conn = nil

    @tools = nil
    # Don't clear auth error as we need it for reporting the correct error
    # Don't reset @consecutive_ping_failures or @reconnect_attempts as they're tracked across reconnections
  end
end

#complete(ref:, argument:, context: nil) ⇒ Hash

Request completion suggestions from the server (MCP 2025-06-18)

Parameters:

  • ref (Hash)

    reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })

  • argument (Hash)

    the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })

  • context (Hash, nil) (defaults to: nil)

    optional context for the completion (MCP 2025-11-25)

Returns:

  • (Hash)

    completion result with 'values', optional 'total', and 'hasMore' fields

Raises:



340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/mcp_client/server_sse.rb', line 340

def complete(ref:, argument:, context: nil)
  ensure_initialized
  require_capability!('completions', method: 'completion/complete')
  params = { ref: ref, argument: argument }
  params[:context] = context if context
  result = rpc_request('completion/complete', params)
  result['completion'] || { 'values' => [] }
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError,
       MCPClient::Errors::CapabilityError
  raise
rescue StandardError => e
  raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
end

#connectBoolean

Connect to the MCP server over HTTP/HTTPS with SSE

Returns:

  • (Boolean)

    true if connection was successful

Raises:



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/mcp_client/server_sse.rb', line 373

def connect
  return true if @mutex.synchronize { @connection_established }

  # Check for pre-existing auth error (needed for tests)
  pre_existing_auth_error = @mutex.synchronize { @auth_error }

  begin
    # Don't reset auth error if it's pre-existing
    @mutex.synchronize { @auth_error = nil } unless pre_existing_auth_error
    # Clear any stale connection failure cause from a previous attempt
    @mutex.synchronize { @connection_error = nil }

    start_sse_thread
    effective_timeout = [@read_timeout || 30, 30].min
    wait_for_connection(timeout: effective_timeout)
    start_activity_monitor
    true
  rescue MCPClient::Errors::ConnectionError => e
    cleanup
    # Simply pass through any ConnectionError without wrapping it again
    # This prevents duplicate error messages in the stack
    raise e
  rescue StandardError => e
    cleanup
    # Check for stored auth error first as it's more specific
    auth_error = @mutex.synchronize { @auth_error }
    raise MCPClient::Errors::ConnectionError, auth_error if auth_error

    raise MCPClient::Errors::ConnectionError, "Failed to connect to MCP server at #{@base_url}: #{e.message}"
  end
end

#get_prompt(prompt_name, parameters) ⇒ Object

Get a prompt with the given parameters

Parameters:

  • prompt_name (String)

    the name of the prompt to get

  • parameters (Hash)

    the parameters to pass to the prompt

Returns:

  • (Object)

    the result of the prompt interpolation

Raises:



165
166
167
168
169
170
171
172
173
# File 'lib/mcp_client/server_sse.rb', line 165

def get_prompt(prompt_name, parameters)
  rpc_request('prompts/get', build_named_request_params(prompt_name, parameters))
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
  # Re-raise connection/transport errors directly to match test expectations
  raise
rescue StandardError => e
  # For all other errors, wrap in PromptGetError
  raise MCPClient::Errors::PromptGetError, "Error get prompt '#{prompt_name}': #{e.message}"
end

#handle_elicitation_create(request_id, params) ⇒ void

This method returns an undefined value.

Handle elicitation/create request from server (MCP 2025-06-18)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • params (Hash)

    the elicitation parameters



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/mcp_client/server_sse.rb', line 545

def handle_elicitation_create(request_id, params)
  # Without a callback there is no user to interact with: answer with a
  # JSON-RPC error rather than fabricating a user "decline".
  unless @elicitation_request_callback
    @logger.warn('Received elicitation request but no callback registered')
    send_error_response(request_id, -32_601, 'Elicitation not supported: no handler configured')
    return
  end

  # Call the registered callback
  result = @elicitation_request_callback.call(request_id, params)

  # Send the response back to the server (echoing related-task _meta)
  send_elicitation_response(request_id, merge_related_task_meta(result, params))
end

#handle_ping(request_id) ⇒ void

This method returns an undefined value.

Handle a server-initiated ping request (MCP ping utility) The receiver MUST respond promptly with an empty result.

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID



530
531
532
533
534
535
536
537
538
539
# File 'lib/mcp_client/server_sse.rb', line 530

def handle_ping(request_id)
  response = {
    'jsonrpc' => '2.0',
    'id' => request_id,
    'result' => {}
  }
  post_jsonrpc_response(response)
rescue StandardError => e
  @logger.error("Error sending ping response: #{e.message}")
end

#handle_roots_list(request_id, params) ⇒ void

This method returns an undefined value.

Handle roots/list request from server (MCP 2025-06-18)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • params (Hash)

    the request parameters



565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/mcp_client/server_sse.rb', line 565

def handle_roots_list(request_id, params)
  # If no callback is registered, return empty roots list
  unless @roots_list_request_callback
    @logger.debug('Received roots/list request but no callback registered, returning empty list')
    send_roots_list_response(request_id, { 'roots' => [] })
    return
  end

  # Call the registered callback
  result = @roots_list_request_callback.call(request_id, params)

  # Send the response back to the server (echoing related-task _meta)
  send_roots_list_response(request_id, merge_related_task_meta(result, params))
end

#handle_sampling_create_message(request_id, params) ⇒ void

This method returns an undefined value.

Handle sampling/createMessage request from server (MCP 2025-11-25)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • params (Hash)

    the sampling parameters



603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'lib/mcp_client/server_sse.rb', line 603

def handle_sampling_create_message(request_id, params)
  # If no callback is registered, return error
  unless @sampling_request_callback
    @logger.warn('Received sampling request but no callback registered, returning error')
    send_error_response(request_id, -1, 'Sampling not supported')
    return
  end

  # Call the registered callback
  result = @sampling_request_callback.call(request_id, params)

  # Send the response back to the server (echoing related-task _meta)
  send_sampling_response(request_id, merge_related_task_meta(result, params))
end

#handle_server_request(msg) ⇒ void

This method returns an undefined value.

Handle incoming JSON-RPC request from server (MCP 2025-06-18)

Parameters:

  • msg (Hash)

    the JSON-RPC request message



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/mcp_client/server_sse.rb', line 501

def handle_server_request(msg)
  request_id = msg['id']
  method = msg['method']
  params = msg['params'] || {}

  @logger.debug("Received server request: #{method} (id: #{request_id})")

  case method
  when 'ping'
    handle_ping(request_id)
  when 'elicitation/create'
    handle_elicitation_create(request_id, params)
  when 'roots/list'
    handle_roots_list(request_id, params)
  when 'sampling/createMessage'
    handle_sampling_create_message(request_id, params)
  else
    # Unknown request method, send error response
    send_error_response(request_id, -32_601, "Method not found: #{method}")
  end
rescue StandardError => e
  @logger.error("Error handling server request: #{e.message}")
  send_error_response(request_id, -32_603, "Internal error: #{e.message}")
end

#list_promptsArray<MCPClient::Prompt>

List all prompts available from the MCP server

Returns:

Raises:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mcp_client/server_sse.rb', line 133

def list_prompts
  @mutex.synchronize do
    return @prompts if @prompts
  end

  begin
    ensure_initialized

    prompts_data = request_prompts_list
    @mutex.synchronize do
      @prompts = prompts_data.map do |prompt_data|
        MCPClient::Prompt.from_json(prompt_data, server: self)
      end
    end

    @mutex.synchronize { @prompts }
  rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
    # Re-raise these errors directly
    raise
  rescue StandardError => e
    raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
  end
end

#list_resource_templates(cursor: nil) ⇒ Hash

List all resource templates available from the MCP server

Parameters:

  • cursor (String, nil) (defaults to: nil)

    optional cursor for pagination

Returns:

  • (Hash)

    result containing resourceTemplates array and optional nextCursor

Raises:



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/mcp_client/server_sse.rb', line 236

def list_resource_templates(cursor: nil)
  ensure_initialized
  params = {}
  params['cursor'] = cursor if cursor
  result = rpc_request('resources/templates/list', params)

  templates = (result['resourceTemplates'] || []).map do |template_data|
    MCPClient::ResourceTemplate.from_json(template_data, server: self)
  end

  { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
  raise
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
end

#list_resources(cursor: nil) ⇒ Hash

List all resources available from the MCP server

Parameters:

  • cursor (String, nil) (defaults to: nil)

    optional cursor for pagination

Returns:

  • (Hash)

    result containing resources array and optional nextCursor

Raises:



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/mcp_client/server_sse.rb', line 181

def list_resources(cursor: nil)
  @mutex.synchronize do
    return @resources_result if @resources_result && !cursor
  end

  begin
    ensure_initialized

    params = {}
    params['cursor'] = cursor if cursor
    result = rpc_request('resources/list', params)

    resources = (result['resources'] || []).map do |resource_data|
      MCPClient::Resource.from_json(resource_data, server: self)
    end

    resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }

    @mutex.synchronize do
      @resources_result = resources_result unless cursor
    end

    resources_result
  rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
    # Re-raise these errors directly
    raise
  rescue StandardError => e
    raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
  end
end

#list_toolsArray<MCPClient::Tool>

List all tools available from the MCP server

Returns:

Raises:



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/mcp_client/server_sse.rb', line 292

def list_tools
  @mutex.synchronize do
    return @tools if @tools
  end

  begin
    ensure_initialized

    tools_data = request_tools_list
    @mutex.synchronize do
      @tools = tools_data.map do |tool_data|
        MCPClient::Tool.from_json(tool_data, server: self)
      end
    end

    @mutex.synchronize { @tools }
  rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
    # Re-raise these errors directly
    raise
  rescue StandardError => e
    raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
  end
end

#log_level=(level) ⇒ Hash

Set the logging level on the server (MCP 2025-06-18)

Parameters:

  • level (String)

    the log level ('debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency')

Returns:

  • (Hash)

    empty result on success

Raises:



359
360
361
362
363
364
365
366
367
368
# File 'lib/mcp_client/server_sse.rb', line 359

def log_level=(level)
  ensure_initialized
  require_capability!('logging', method: 'logging/setLevel')
  rpc_request('logging/setLevel', { level: level })
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError,
       MCPClient::Errors::CapabilityError
  raise
rescue StandardError => e
  raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
end

#on_elicitation_request(&block) ⇒ void

This method returns an undefined value.

Register a callback for elicitation requests (MCP 2025-06-18)

Parameters:

  • block (Proc)

    callback that receives (request_id, params) and returns response hash



480
481
482
# File 'lib/mcp_client/server_sse.rb', line 480

def on_elicitation_request(&block)
  @elicitation_request_callback = block
end

#on_roots_list_request(&block) ⇒ void

This method returns an undefined value.

Register a callback for roots/list requests (MCP 2025-06-18)

Parameters:

  • block (Proc)

    callback that receives (request_id, params) and returns response hash



487
488
489
# File 'lib/mcp_client/server_sse.rb', line 487

def on_roots_list_request(&block)
  @roots_list_request_callback = block
end

#on_sampling_request(&block) ⇒ void

This method returns an undefined value.

Register a callback for sampling requests (MCP 2025-11-25)

Parameters:

  • block (Proc)

    callback that receives (request_id, params) and returns response hash



494
495
496
# File 'lib/mcp_client/server_sse.rb', line 494

def on_sampling_request(&block)
  @sampling_request_callback = block
end

#post_jsonrpc_response(response) ⇒ void

This method returns an undefined value.

Post a JSON-RPC response message to the server via HTTP

Parameters:

  • response (Hash)

    the JSON-RPC response



697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
# File 'lib/mcp_client/server_sse.rb', line 697

def post_jsonrpc_response(response)
  unless @rpc_endpoint
    @logger.error('Cannot send response: RPC endpoint not available')
    return
  end

  # Use the same connection pattern as post_json_rpc_request
  uri = URI.parse(@base_url)
  base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
  @rpc_conn ||= create_json_rpc_connection(base)

  json_body = JSON.generate(response)

  @rpc_conn.post do |req|
    req.url @rpc_endpoint
    req.headers['Content-Type'] = 'application/json'
    # MCP lifecycle "Version Negotiation": include the MCP-Protocol-Version
    # header on all HTTP requests after the initialize handshake.
    req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
    @headers.each { |k, v| req.headers[k] = v }
    req.body = json_body
  end

  @logger.debug("Sent response via HTTP POST: #{json_body}")
rescue StandardError => e
  @logger.error("Failed to send response via HTTP POST: #{e.message}")
end

#read_resource(uri) ⇒ Array<MCPClient::ResourceContent>

Read a resource by its URI

Parameters:

  • uri (String)

    the URI of the resource to read

Returns:

Raises:



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/mcp_client/server_sse.rb', line 219

def read_resource(uri)
  result = rpc_request('resources/read', { uri: uri })
  contents = result['contents'] || []
  contents.map { |content| MCPClient::ResourceContent.from_json(content) }
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
  # Re-raise connection/transport errors directly to match test expectations
  raise
rescue StandardError => e
  # For all other errors, wrap in ResourceReadError
  raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
end

#send_elicitation_response(request_id, result) ⇒ void

This method returns an undefined value.

Send elicitation response back to server via HTTP POST (MCP 2025-06-18)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • result (Hash)

    the elicitation result (action and optional content)



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/mcp_client/server_sse.rb', line 647

def send_elicitation_response(request_id, result)
  # Error-shaped results become JSON-RPC error responses (e.g. -32602 for
  # an undeclared elicitation mode), mirroring the sampling error path.
  if result.is_a?(Hash) && result['error']
    send_error_response(request_id, result['error']['code'] || -32_603,
                        result['error']['message'] || 'Elicitation error')
    return
  end

  ensure_initialized

  response = {
    'jsonrpc' => '2.0',
    'id' => request_id,
    'result' => result
  }

  # Send response via HTTP POST to the RPC endpoint
  post_jsonrpc_response(response)
rescue StandardError => e
  @logger.error("Error sending elicitation response: #{e.message}")
end

#send_error_response(request_id, code, message) ⇒ void

This method returns an undefined value.

Send error response back to server via HTTP POST (MCP 2025-06-18)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • code (Integer)

    the error code

  • message (String)

    the error message



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/mcp_client/server_sse.rb', line 675

def send_error_response(request_id, code, message)
  ensure_initialized

  response = {
    'jsonrpc' => '2.0',
    'id' => request_id,
    'error' => {
      'code' => code,
      'message' => message
    }
  }

  # Send response via HTTP POST to the RPC endpoint
  post_jsonrpc_response(response)
rescue StandardError => e
  @logger.error("Error sending error response: #{e.message}")
end

#send_roots_list_response(request_id, result) ⇒ void

This method returns an undefined value.

Send roots/list response back to server via HTTP POST (MCP 2025-06-18)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • result (Hash)

    the roots list result



584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/mcp_client/server_sse.rb', line 584

def send_roots_list_response(request_id, result)
  ensure_initialized

  response = {
    'jsonrpc' => '2.0',
    'id' => request_id,
    'result' => result
  }

  # Send response via HTTP POST to the RPC endpoint
  post_jsonrpc_response(response)
rescue StandardError => e
  @logger.error("Error sending roots/list response: #{e.message}")
end

#send_sampling_response(request_id, result) ⇒ void

This method returns an undefined value.

Send sampling response back to server via HTTP POST (MCP 2025-11-25)

Parameters:

  • request_id (String, Integer)

    the JSON-RPC request ID

  • result (Hash)

    the sampling result (role, content, model, stopReason)



622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
# File 'lib/mcp_client/server_sse.rb', line 622

def send_sampling_response(request_id, result)
  ensure_initialized

  # Check if result contains an error
  if result.is_a?(Hash) && result['error']
    send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
    return
  end

  response = {
    'jsonrpc' => '2.0',
    'id' => request_id,
    'result' => result
  }

  # Send response via HTTP POST to the RPC endpoint
  post_jsonrpc_response(response)
rescue StandardError => e
  @logger.error("Error sending sampling response: #{e.message}")
end

#subscribe_resource(uri) ⇒ Boolean

Subscribe to resource updates

Parameters:

  • uri (String)

    the URI of the resource to subscribe to

Returns:

  • (Boolean)

    true if subscription successful

Raises:



258
259
260
261
262
263
264
265
266
267
268
# File 'lib/mcp_client/server_sse.rb', line 258

def subscribe_resource(uri)
  ensure_initialized
  require_capability!('resources', 'subscribe', method: 'resources/subscribe')
  rpc_request('resources/subscribe', { uri: uri })
  true
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError,
       MCPClient::Errors::CapabilityError
  raise
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
end

#unsubscribe_resource(uri) ⇒ Boolean

Unsubscribe from resource updates

Parameters:

  • uri (String)

    the URI of the resource to unsubscribe from

Returns:

  • (Boolean)

    true if unsubscription successful

Raises:



275
276
277
278
279
280
281
282
283
284
285
# File 'lib/mcp_client/server_sse.rb', line 275

def unsubscribe_resource(uri)
  ensure_initialized
  require_capability!('resources', 'subscribe', method: 'resources/unsubscribe')
  rpc_request('resources/unsubscribe', { uri: uri })
  true
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError,
       MCPClient::Errors::CapabilityError
  raise
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
end