Module: MCPClient::JsonRpcCommon
- Defined in:
- lib/mcp_client/json_rpc_common.rb
Overview
Shared retry/backoff logic for JSON-RPC transports
Instance Method Summary collapse
-
#build_jsonrpc_notification(method, params) ⇒ Hash
Build a JSON-RPC notification object (no response expected).
-
#build_jsonrpc_request(method, params, id) ⇒ Hash
Build a JSON-RPC request object.
-
#build_named_request_params(name, arguments) ⇒ Hash
Build tools/call- or prompts/get-style params with request-level _meta hoisted out of the arguments (string keys, matching the JSON wire form).
-
#cancellable_request?(method, params) ⇒ Boolean
Whether automatic notifications/cancelled on timeout is appropriate for this request: never for initialize (MUST NOT be cancelled), and never for task-augmented requests (tasks use tasks/cancel instead).
-
#client_capabilities ⇒ Hash
Declared client capabilities, derived from the server-request callbacks the host actually registered before connecting.
-
#client_info_payload ⇒ Hash
The Implementation object sent as clientInfo: the host-provided info when configured (client_info=), otherwise the gem's identity.
-
#declare_sampling_tools ⇒ void
Opt this transport into declaring tool-use support for sampling (ClientCapabilities.sampling.tools, MCP 2025-11-25 / SEP-1577).
-
#initialization_params ⇒ Hash
Generate initialization parameters for MCP protocol.
-
#ping ⇒ Hash
Ping the server to keep the connection alive.
-
#process_jsonrpc_response(response) ⇒ Object
Process JSON-RPC response.
-
#registered_callback?(ivar) ⇒ Boolean
Whether the callback is registered on this transport.
-
#sampling_tools_supported? ⇒ Boolean
Whether the host opted into sampling tool use.
-
#split_request_meta(arguments) ⇒ Array(Hash, Hash|nil)
Split request-level _meta (RequestParams._meta, e.g. progressToken or related-task metadata) out of user-supplied tool/prompt arguments.
-
#validate_protocol_version!(result) ⇒ String
Validate the protocol version the server negotiated in its initialize result.
-
#with_retry { ... } ⇒ Object
Execute the block with retry/backoff for transient errors only.
Instance Method Details
#build_jsonrpc_notification(method, params) ⇒ Hash
Build a JSON-RPC notification object (no response expected)
108 109 110 111 112 113 114 |
# File 'lib/mcp_client/json_rpc_common.rb', line 108 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
95 96 97 98 99 100 101 102 |
# File 'lib/mcp_client/json_rpc_common.rb', line 95 def build_jsonrpc_request(method, params, id) { 'jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params } end |
#build_named_request_params(name, arguments) ⇒ Hash
Build tools/call- or prompts/get-style params with request-level _meta hoisted out of the arguments (string keys, matching the JSON wire form).
83 84 85 86 87 88 |
# File 'lib/mcp_client/json_rpc_common.rb', line 83 def build_named_request_params(name, arguments) args, = (arguments) params = { 'name' => name, 'arguments' => args } params['_meta'] = if params end |
#cancellable_request?(method, params) ⇒ Boolean
Whether automatic notifications/cancelled on timeout is appropriate for this request: never for initialize (MUST NOT be cancelled), and never for task-augmented requests (tasks use tasks/cancel instead).
56 57 58 59 60 61 |
# File 'lib/mcp_client/json_rpc_common.rb', line 56 def cancellable_request?(method, params) return false if method == 'initialize' return false if params.is_a?(Hash) && (params.key?('task') || params.key?(:task)) true end |
#client_capabilities ⇒ Hash
Declared client capabilities, derived from the server-request callbacks the host actually registered before connecting. Per MCP 2025-11-25, clients that support a feature MUST declare it during initialization, and only negotiated capabilities may be used afterwards — so declaring a hardcoded set independent of host support violates the lifecycle in both directions.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/mcp_client/json_rpc_common.rb', line 164 def client_capabilities capabilities = {} if registered_callback?(:@elicitation_request_callback) # Both defined elicitation modes are implemented (an empty object # would mean form-only per the spec's backwards-compatibility rule). capabilities['elicitation'] = { 'form' => {}, 'url' => {} } end capabilities['roots'] = { 'listChanged' => true } if registered_callback?(:@roots_list_request_callback) if registered_callback?(:@sampling_request_callback) # SEP-1577: servers may only send tool-enabled sampling requests when # the client declares the sampling.tools sub-capability. capabilities['sampling'] = sampling_tools_supported? ? { 'tools' => {} } : {} end # 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. capabilities end |
#client_info_payload ⇒ Hash
The Implementation object sent as clientInfo: the host-provided info when configured (client_info=), otherwise the gem's identity.
151 152 153 154 155 |
# File 'lib/mcp_client/json_rpc_common.rb', line 151 def client_info_payload return @client_info if defined?(@client_info) && @client_info { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION } end |
#declare_sampling_tools ⇒ void
This method returns an undefined value.
Opt this transport into declaring tool-use support for sampling (ClientCapabilities.sampling.tools, MCP 2025-11-25 / SEP-1577). Call before connect so the initialize request advertises it; it only takes effect when a sampling request callback is also registered, since sampling.tools is a sub-capability of sampling.
191 192 193 |
# File 'lib/mcp_client/json_rpc_common.rb', line 191 def declare_sampling_tools @sampling_tools_supported = true end |
#initialization_params ⇒ Hash
Generate initialization parameters for MCP protocol
118 119 120 121 122 123 124 |
# File 'lib/mcp_client/json_rpc_common.rb', line 118 def initialization_params { 'protocolVersion' => MCPClient::PROTOCOL_VERSION, 'capabilities' => client_capabilities, 'clientInfo' => client_info_payload } end |
#ping ⇒ Hash
Ping the server to keep the connection alive
46 47 48 |
# File 'lib/mcp_client/json_rpc_common.rb', line 46 def ping rpc_request('ping') end |
#process_jsonrpc_response(response) ⇒ Object
Process JSON-RPC response
210 211 212 213 214 |
# File 'lib/mcp_client/json_rpc_common.rb', line 210 def process_jsonrpc_response(response) raise MCPClient::Errors::ServerError, response['error']['message'] if response['error'] response['result'] end |
#registered_callback?(ivar) ⇒ Boolean
Returns whether the callback is registered on this transport.
197 198 199 |
# File 'lib/mcp_client/json_rpc_common.rb', line 197 def registered_callback?(ivar) instance_variable_defined?(ivar) && !instance_variable_get(ivar).nil? end |
#sampling_tools_supported? ⇒ Boolean
Returns whether the host opted into sampling tool use.
202 203 204 |
# File 'lib/mcp_client/json_rpc_common.rb', line 202 def sampling_tools_supported? instance_variable_defined?(:@sampling_tools_supported) && @sampling_tools_supported end |
#split_request_meta(arguments) ⇒ Array(Hash, Hash|nil)
Split request-level _meta (RequestParams._meta, e.g. progressToken or related-task metadata) out of user-supplied tool/prompt arguments. Accepts both :_meta and '_meta' key spellings; per MCP, _meta belongs at the request params level, not inside the tool's arguments.
69 70 71 72 73 74 75 76 |
# File 'lib/mcp_client/json_rpc_common.rb', line 69 def (arguments) return [arguments, nil] unless arguments.is_a?(Hash) = arguments[:_meta] || arguments['_meta'] return [arguments, nil] unless [arguments.except(:_meta, '_meta'), ] end |
#validate_protocol_version!(result) ⇒ String
Validate the protocol version the server negotiated in its initialize result. Per the MCP lifecycle, the server may answer with a different version than requested; if the client cannot support it, it MUST disconnect. Disconnects (via the transport's cleanup) and raises when the version is unsupported or absent.
134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/mcp_client/json_rpc_common.rb', line 134 def validate_protocol_version!(result) version = result['protocolVersion'] return version if MCPClient::SUPPORTED_PROTOCOL_VERSIONS.include?(version) begin cleanup if respond_to?(:cleanup) rescue StandardError => e @logger.debug("Cleanup after protocol version mismatch failed: #{e.}") end raise MCPClient::Errors::ConnectionError, "Server negotiated unsupported protocol version #{version.inspect} " \ "(supported: #{MCPClient::SUPPORTED_PROTOCOL_VERSIONS.join(', ')}); disconnecting" 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.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# 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 # A timed-out request may still be executing server-side; re-sending # it could run a non-idempotent operation twice. Never retry those. raise if e.is_a?(MCPClient::Errors::RequestTimeoutError) attempts += 1 if attempts <= @max_retries delay = @retry_backoff * (2**(attempts - 1)) @logger.debug("Retry attempt #{attempts} after error: #{e.}, sleeping #{delay}s") sleep(delay) retry end raise end end |