Module: MCPClient::HttpTransportBase
- Includes:
- JsonRpcCommon
- Defined in:
- lib/mcp_client/http_transport_base.rb
Overview
Base module for HTTP-based JSON-RPC transports Contains common functionality shared between HTTP and Streamable HTTP transports
Defined Under Namespace
Classes: NormalizedResponse
Constant Summary collapse
- AUTH_PARAM =
One auth-param (name = token / quoted-string) as it appears in a WWW-Authenticate challenge (RFC 7235 §2.1, optional whitespace around '=').
/[A-Za-z0-9._~+-]+\s*=\s*(?:"(?:[^"\\]|\\.)*"|[^,\s]*)/- AUTH_PARAMS_RUN =
A run of comma/space separated auth-params anchored at the start of a string. The run ends before a token that is NOT followed by '=' — the auth-scheme introducing the next challenge — while commas inside quoted values are consumed by the quoted-string branch, not treated as boundaries.
/\A(?:[\s,]*#{AUTH_PARAM})*/
Constants included from JsonRpcCommon
JsonRpcCommon::NON_IDEMPOTENT_METHODS
Instance Method Summary collapse
-
#resend_after_session_restart(request) ⇒ Faraday::Response
Resend a request against the freshly restarted session — unless doing so could execute a side effect twice.
-
#rpc_notify(method, params = {}) ⇒ void
Send a JSON-RPC notification (no response expected).
-
#rpc_request(method, params = {}, timeout: nil) ⇒ Object
Generic JSON-RPC request: send method with params and return result.
-
#send_cancellation_notification(request_id) ⇒ void
Best-effort notifications/cancelled for a request the client stopped waiting on.
-
#terminate_session ⇒ Boolean
Terminate the current session with the server Sends an HTTP DELETE request with the session ID to properly close the session.
-
#valid_server_url?(url) ⇒ Boolean
Validate the server's base URL for security.
-
#valid_session_id?(session_id) ⇒ Boolean
Validate session ID format Per MCP 2025-11-25, the server-assigned session ID "MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E)" — e.g.
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
Instance Method Details
#resend_after_session_restart(request) ⇒ Faraday::Response
Resend a request against the freshly restarted session — unless doing so could execute a side effect twice.
A 404 usually means the server rejected the request outright, but it does not prove that: a session can expire after the tool ran. Automatic session recovery is worth having for idempotent methods, and would otherwise be a hole straight through the no-replay guarantee that with_retry enforces for NON_IDEMPOTENT_METHODS.
Raises ConnectionError (which with_retry never retries) so no other path can turn this into a second attempt.
129 130 131 132 133 134 135 136 |
# File 'lib/mcp_client/http_transport_base.rb', line 129 def resend_after_session_restart(request) method = request['method'] return send_http_request(request) unless NON_IDEMPOTENT_METHODS.include?(method) raise MCPClient::Errors::ConnectionError, "Session expired during #{method}; a new session was started but the request was NOT resent " \ 'because it may already have executed. Retry it explicitly if that is safe.' end |
#rpc_notify(method, params = {}) ⇒ void
This method returns an undefined value.
Send a JSON-RPC notification (no response expected)
66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/mcp_client/http_transport_base.rb', line 66 def rpc_notify(method, params = {}) ensure_connected notif = build_jsonrpc_notification(method, params) begin send_http_request(notif) rescue MCPClient::Errors::ServerError, MCPClient::Errors::ConnectionError, Faraday::ConnectionFailed => e raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.}" end end |
#rpc_request(method, params = {}, timeout: nil) ⇒ Object
Generic JSON-RPC request: send method with params and return result
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/mcp_client/http_transport_base.rb', line 33 def rpc_request(method, params = {}, timeout: nil) ensure_connected with_retry(method) do request_id = @mutex.synchronize { @request_id += 1 } request = build_jsonrpc_request(method, params, request_id) begin send_jsonrpc_request(request, timeout: timeout) rescue MCPClient::Errors::RequestTimeoutError # MCP lifecycle: on timeout the sender SHOULD issue a cancellation # notification for the abandoned request and stop waiting. send_cancellation_notification(request_id) if cancellable_request?(method, params) raise end end end |
#send_cancellation_notification(request_id) ⇒ void
This method returns an undefined value.
Best-effort notifications/cancelled for a request the client stopped waiting on. Failures are swallowed.
54 55 56 57 58 59 60 |
# File 'lib/mcp_client/http_transport_base.rb', line 54 def send_cancellation_notification(request_id) notif = build_jsonrpc_notification('notifications/cancelled', { 'requestId' => request_id, 'reason' => 'Request timed out' }) send_http_request(notif) rescue StandardError => e @logger.debug("Failed to send cancellation notification: #{e.}") end |
#terminate_session ⇒ Boolean
Terminate the current session with the server Sends an HTTP DELETE request with the session ID to properly close the session
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 |
# File 'lib/mcp_client/http_transport_base.rb', line 82 def terminate_session return true unless @session_id conn = http_connection begin @logger.debug("Terminating session: #{@session_id}") response = conn.delete(@endpoint) do |req| # Apply base headers but prioritize session termination headers @headers.each { |k, v| req.headers[k] = v } req.headers['Mcp-Session-Id'] = @session_id req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version # MCP: authorization MUST be included in every HTTP request @oauth_provider&.(req) end if response.success? @logger.debug("Session terminated successfully: #{@session_id}") @session_id = nil true else @logger.warn("Session termination failed with HTTP #{response.status}") @session_id = nil # Clear session ID even on HTTP error false end rescue Faraday::Error => e @logger.warn("Session termination request failed: #{e.}") # Clear session ID even if termination request failed @session_id = nil false end end |
#valid_server_url?(url) ⇒ Boolean
Validate the server's base URL for security
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/mcp_client/http_transport_base.rb', line 156 def valid_server_url?(url) return false unless url.is_a?(String) uri = URI.parse(url) # Only allow HTTP and HTTPS protocols return false unless %w[http https].include?(uri.scheme) # Must have a host return false if uri.host.nil? || uri.host.empty? # Don't allow localhost binding to all interfaces in production if uri.host == '0.0.0.0' @logger.warn('Server URL uses 0.0.0.0 which may be insecure. Consider using 127.0.0.1 for localhost.') end true rescue URI::InvalidURIError false end |
#valid_session_id?(session_id) ⇒ Boolean
Validate session ID format Per MCP 2025-11-25, the server-assigned session ID "MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E)" — e.g. a UUID, a JWT, or a cryptographic hash — and the client MUST echo whatever the server assigned. A generous length cap guards against abuse.
145 146 147 148 149 150 151 |
# File 'lib/mcp_client/http_transport_base.rb', line 145 def valid_session_id?(session_id) return false unless session_id.is_a?(String) # The 4096-char cap is header-size hygiene, not MCP grammar — the spec # imposes no length limit on session IDs. session_id.match?(/\A[\x21-\x7E]{1,4096}\z/) end |