Module: MCPClient::HttpTransportBase

Includes:
JsonRpcCommon
Included in:
ServerHTTP::JsonRpcTransport, ServerStreamableHTTP::JsonRpcTransport
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})*/

Instance Method Summary collapse

Methods included from JsonRpcCommon

#build_jsonrpc_notification, #build_jsonrpc_request, #build_named_request_params, #cancellable_request?, #client_capabilities, #client_info_payload, #declare_sampling_tools, #initialization_params, #ping, #process_jsonrpc_response, #registered_callback?, #sampling_tools_supported?, #split_request_meta, #validate_protocol_version!, #with_retry

Instance Method Details

#rpc_notify(method, params = {}) ⇒ void

This method returns an undefined value.

Send a JSON-RPC notification (no response expected)

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash) (defaults to: {})

    parameters for the notification



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.message}"
  end
end

#rpc_request(method, params = {}, timeout: nil) ⇒ Object

Generic JSON-RPC request: send method with params and return result

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash) (defaults to: {})

    parameters for the request

Returns:

  • (Object)

    result from JSON-RPC response

Raises:



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 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.

Parameters:

  • request_id (Integer)

    id of the abandoned request



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.message}")
end

#terminate_sessionBoolean

Terminate the current session with the server Sends an HTTP DELETE request with the session ID to properly close the session

Returns:

  • (Boolean)

    true if termination was successful

Raises:



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&.apply_authorization(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.message}")
    # 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

Parameters:

  • url (String)

    the URL to validate

Returns:

  • (Boolean)

    true if URL is considered safe



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/mcp_client/http_transport_base.rb', line 133

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.

Parameters:

  • session_id (String)

    the session ID to validate

Returns:

  • (Boolean)

    true if session ID is valid



122
123
124
125
126
127
128
# File 'lib/mcp_client/http_transport_base.rb', line 122

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