Module: MCPClient::JsonRpcCommon

Included in:
HttpTransportBase, ServerSSE::JsonRpcTransport, ServerStdio::JsonRpcTransport
Defined in:
lib/mcp_client/json_rpc_common.rb

Overview

Shared retry/backoff logic for JSON-RPC transports

Instance Method Summary collapse

Instance Method Details

#build_jsonrpc_notification(method, params) ⇒ Hash

Build a JSON-RPC notification object (no response expected)

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash)

    parameters for the notification

Returns:

  • (Hash)

    the JSON-RPC notification object



64
65
66
67
68
69
70
# File 'lib/mcp_client/json_rpc_common.rb', line 64

def build_jsonrpc_notification(method, params)
  {
    'jsonrpc' => '2.0',
    'method' => method,
    'params' => params
  }
end

#build_jsonrpc_request(method, params, id) ⇒ Hash

Build a JSON-RPC request object

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash)

    parameters for the request

  • id (Integer)

    request ID

Returns:

  • (Hash)

    the JSON-RPC request object



51
52
53
54
55
56
57
58
# File 'lib/mcp_client/json_rpc_common.rb', line 51

def build_jsonrpc_request(method, params, id)
  {
    'jsonrpc' => '2.0',
    'id' => id,
    'method' => method,
    'params' => params
  }
end

#initialization_paramsHash

Generate initialization parameters for MCP protocol

Returns:

  • (Hash)

    the initialization parameters



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/mcp_client/json_rpc_common.rb', line 74

def initialization_params
  capabilities = {
    'elicitation' => {}, # MCP 2025-11-25: Support for server-initiated user interactions
    'roots' => { 'listChanged' => true }, # MCP 2025-11-25: Support for roots
    'sampling' => {} # MCP 2025-11-25: Support for server-initiated LLM sampling
    # NOTE: we intentionally do NOT declare a client `tasks` capability. That
    # capability marks the client as a RECEIVER of task-augmented
    # sampling/elicitation requests, which is not implemented here — this
    # client only acts as a task REQUESTOR for tools/call (see
    # Client#call_tool_as_task), which requires no client-side declaration.
  }

  {
    'protocolVersion' => MCPClient::PROTOCOL_VERSION,
    'capabilities' => capabilities,
    'clientInfo' => { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION }
  }
end

#pingHash

Ping the server to keep the connection alive

Returns:

  • (Hash)

    the result of the ping request

Raises:



42
43
44
# File 'lib/mcp_client/json_rpc_common.rb', line 42

def ping
  rpc_request('ping')
end

#process_jsonrpc_response(response) ⇒ Object

Process JSON-RPC response

Parameters:

  • response (Hash)

    the parsed JSON-RPC response

Returns:

  • (Object)

    the result field from the response

Raises:



97
98
99
100
101
# File 'lib/mcp_client/json_rpc_common.rb', line 97

def process_jsonrpc_response(response)
  raise MCPClient::Errors::ServerError, response['error']['message'] if response['error']

  response['result']
end

#with_retry { ... } ⇒ Object

Execute the block with retry/backoff for transient errors only.

Retries genuinely transient failures where the request most likely did not complete at the server: transport/network errors (TransportError, IOError, Errno::ETIMEDOUT/ECONNRESET/EPIPE) and TransientServerError (HTTP 5xx).

It deliberately does NOT retry a plain ServerError. A plain ServerError is raised for a JSON-RPC error response or an HTTP 4xx — cases where the server received and processed (or deterministically rejected) the request. Re-sending those would silently re-execute a non-idempotent operation (e.g. a tools/call), which JSON-RPC provides no way to make safe.

Yields:

  • block to execute

Returns:

  • (Object)

    result of block

Raises:

  • original exception if max retries exceeded or the error is not retryable



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/mcp_client/json_rpc_common.rb', line 20

def with_retry
  attempts = 0
  begin
    yield
  rescue MCPClient::Errors::TransientServerError, MCPClient::Errors::TransportError, IOError,
         Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::EPIPE => e
    attempts += 1
    if attempts <= @max_retries
      delay = @retry_backoff * (2**(attempts - 1))
      @logger.debug("Retry attempt #{attempts} after error: #{e.message}, sleeping #{delay}s")
      sleep(delay)
      retry
    end
    raise
  end
end