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

Instance Attribute Summary collapse

Attributes inherited from ServerBase

#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

Methods included from JsonRpcCommon

#build_jsonrpc_notification, #build_jsonrpc_request, #initialization_params, #ping, #process_jsonrpc_response, #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?

Methods inherited from ServerBase

#on_notification, #ping, #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
# 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
  @auth_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:



317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/mcp_client/server_sse.rb', line 317

def call_tool(tool_name, parameters)
  rpc_request('tools/call', {
                name: tool_name,
                arguments: 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)


116
117
118
119
120
# File 'lib/mcp_client/server_sse.rb', line 116

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



400
401
402
403
404
405
406
407
408
409
410
411
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
# File 'lib/mcp_client/server_sse.rb', line 400

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

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



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

def complete(ref:, argument:, context: nil)
  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
  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:



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/mcp_client/server_sse.rb', line 363

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

    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:



159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/mcp_client/server_sse.rb', line 159

def get_prompt(prompt_name, parameters)
  rpc_request('prompts/get', {
                name: prompt_name,
                arguments: 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



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

def handle_elicitation_create(request_id, params)
  # If no callback is registered, decline the request
  unless @elicitation_request_callback
    @logger.warn('Received elicitation request but no callback registered, declining')
    send_elicitation_response(request_id, { 'action' => 'decline' })
    return
  end

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

  # Send the response back to the server
  send_elicitation_response(request_id, result)
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



514
515
516
517
518
519
520
521
522
523
# File 'lib/mcp_client/server_sse.rb', line 514

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



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

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
  send_roots_list_response(request_id, result)
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



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

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
  send_sampling_response(request_id, result)
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



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/mcp_client/server_sse.rb', line 485

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:



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/mcp_client/server_sse.rb', line 127

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:



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

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:



178
179
180
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
# File 'lib/mcp_client/server_sse.rb', line 178

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:



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/mcp_client/server_sse.rb', line 285

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:



352
353
354
355
356
357
358
# File 'lib/mcp_client/server_sse.rb', line 352

def log_level=(level)
  rpc_request('logging/setLevel', { level: level })
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
  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



464
465
466
# File 'lib/mcp_client/server_sse.rb', line 464

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



471
472
473
# File 'lib/mcp_client/server_sse.rb', line 471

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



478
479
480
# File 'lib/mcp_client/server_sse.rb', line 478

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



672
673
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 672

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'
    @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:



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/mcp_client/server_sse.rb', line 216

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)



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

def send_elicitation_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 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



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

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



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

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)



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

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:



255
256
257
258
259
260
261
262
263
# File 'lib/mcp_client/server_sse.rb', line 255

def subscribe_resource(uri)
  ensure_initialized
  rpc_request('resources/subscribe', { uri: uri })
  true
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
  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:



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

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