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/origin_policy.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, OriginPolicy, 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
MAX_SSE_BUFFER_BYTES =

Maximum bytes the SSE parse buffer may hold while waiting for an event terminator. The stream is peer-controlled: without a cap, a hostile server could withhold the blank-line delimiter forever and grow the buffer until the host runs out of memory. Generous enough for any legitimate JSON-RPC response event.

32 * 1024 * 1024

Constants included from JsonRpcCommon

JsonRpcCommon::NON_IDEMPOTENT_METHODS

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 OriginPolicy

#origin_of, #reject_cross_origin_redirect!, #same_origin?

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, #describe_body_size, #describe_jsonrpc_message, #describe_parse_error, #initialization_params, #ping, #process_jsonrpc_response, #registered_callback?, #sampling_tools_supported?, #split_request_meta, #validate_protocol_version!, #with_retry

Methods included from SseParser

#fail_endpoint_handshake!, #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



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
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/mcp_client/server_sse.rb', line 76

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 = {}
  # Ids of requests a caller is actively waiting on. Only responses for
  # these ids are stored in @sse_results — everything else on the peer
  # controlled stream is unsolicited and discarded.
  @pending_request_ids = Set.new
  @mutex = Monitor.new
  @buffer = +''
  # How much of @buffer has already been searched for an event terminator
  @buffer_scanned = 0
  @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



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

def base_url
  @base_url
end

#capabilitiesHash? (readonly)

Server capabilities from initialize response

Returns:

  • (Hash, nil)

    Server capabilities



66
67
68
# File 'lib/mcp_client/server_sse.rb', line 66

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)



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

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

#resourcesObject (readonly)

Returns the value of attribute resources.



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

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

#server_infoHash? (readonly)

Server information from initialize response

Returns:

  • (Hash, nil)

    Server information



62
63
64
# File 'lib/mcp_client/server_sse.rb', line 62

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)



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

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:



337
338
339
340
341
342
343
344
345
# File 'lib/mcp_client/server_sse.rb', line 337

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)


135
136
137
138
139
# File 'lib/mcp_client/server_sse.rb', line 135

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



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
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/mcp_client/server_sse.rb', line 425

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 = +''
    @buffer_scanned = 0

    # Drop results nobody is waiting for, so peer-supplied state cannot
    # accumulate across reconnects. Results for still-pending requests are
    # KEPT: a response can arrive while its POST is still returning, and
    # the waiter (which reconnects through ensure_sse_connection_active)
    # is about to consume it. Discarding those reported a timeout for a
    # tool call the server had already executed — inviting a duplicate
    # manual retry. unregister_pending_request clears each entry when its
    # request finishes, so nothing lingers.
    @sse_results.select! { |id, _| @pending_request_ids.include?(id) }

    # 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:



353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/mcp_client/server_sse.rb', line 353

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:



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/mcp_client/server_sse.rb', line 386

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:



178
179
180
181
182
183
184
185
186
# File 'lib/mcp_client/server_sse.rb', line 178

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



572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
# File 'lib/mcp_client/server_sse.rb', line 572

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



557
558
559
560
561
562
563
564
565
566
# File 'lib/mcp_client/server_sse.rb', line 557

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



592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/mcp_client/server_sse.rb', line 592

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



630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'lib/mcp_client/server_sse.rb', line 630

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



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/mcp_client/server_sse.rb', line 525

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
  # The exception message is host-internal (file paths, connection
  # strings, library internals): log it locally, but answer the peer with
  # a constant message so failures cannot be used to probe the host.
  @logger.error("Error handling server request: #{e.message}")
  send_error_response(request_id, -32_603, 'Internal error')
end

#list_promptsArray<MCPClient::Prompt>

List all prompts available from the MCP server

Returns:

Raises:



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/mcp_client/server_sse.rb', line 146

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:



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/mcp_client/server_sse.rb', line 249

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:



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/mcp_client/server_sse.rb', line 194

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:



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/mcp_client/server_sse.rb', line 305

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:



372
373
374
375
376
377
378
379
380
381
# File 'lib/mcp_client/server_sse.rb', line 372

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



504
505
506
# File 'lib/mcp_client/server_sse.rb', line 504

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



511
512
513
# File 'lib/mcp_client/server_sse.rb', line 511

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



518
519
520
# File 'lib/mcp_client/server_sse.rb', line 518

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



724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
# File 'lib/mcp_client/server_sse.rb', line 724

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: #{describe_jsonrpc_message(response)}")
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:



232
233
234
235
236
237
238
239
240
241
242
# File 'lib/mcp_client/server_sse.rb', line 232

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)



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

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



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# File 'lib/mcp_client/server_sse.rb', line 702

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



611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/mcp_client/server_sse.rb', line 611

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)



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 649

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:



271
272
273
274
275
276
277
278
279
280
281
# File 'lib/mcp_client/server_sse.rb', line 271

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:



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/mcp_client/server_sse.rb', line 288

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