Class: MCP::Client::HTTP
- Inherits:
-
Object
- Object
- MCP::Client::HTTP
- Defined in:
- lib/mcp/client/http.rb
Overview
TODO: HTTP GET for SSE streaming is not yet implemented.
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server
TODO: Resumability and redelivery with Last-Event-ID is not yet implemented.
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redelivery
Constant Summary collapse
- ACCEPT_HEADER =
"application/json, text/event-stream"- SESSION_ID_HEADER =
"Mcp-Session-Id"- PROTOCOL_VERSION_HEADER =
"MCP-Protocol-Version"
Instance Attribute Summary collapse
-
#protocol_version ⇒ Object
readonly
Returns the value of attribute protocol_version.
-
#server_info ⇒ Object
readonly
Returns the value of attribute server_info.
-
#session_id ⇒ Object
readonly
Returns the value of attribute session_id.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Instance Method Summary collapse
-
#close ⇒ Object
Terminates the session by sending an HTTP DELETE to the MCP endpoint with the current ‘Mcp-Session-Id` header, and clears locally tracked session state afterward.
-
#connect(client_info: nil, protocol_version: nil, capabilities: {}) ⇒ Hash
Performs the MCP ‘initialize` handshake: sends an `initialize` request followed by the required `notifications/initialized` notification.
-
#connected? ⇒ Boolean
Returns true once ‘connect` has completed the full handshake (`initialize` response received and `notifications/initialized` sent).
-
#initialize(url:, headers: {}, &block) ⇒ HTTP
constructor
A new instance of HTTP.
-
#send_request(request:) ⇒ Object
Sends a JSON-RPC request and returns the parsed response body.
Constructor Details
#initialize(url:, headers: {}, &block) ⇒ HTTP
Returns a new instance of HTTP.
22 23 24 25 26 27 28 29 30 |
# File 'lib/mcp/client/http.rb', line 22 def initialize(url:, headers: {}, &block) @url = url @headers = headers @faraday_customizer = block @session_id = nil @protocol_version = nil @server_info = nil @connected = false end |
Instance Attribute Details
#protocol_version ⇒ Object (readonly)
Returns the value of attribute protocol_version.
20 21 22 |
# File 'lib/mcp/client/http.rb', line 20 def protocol_version @protocol_version end |
#server_info ⇒ Object (readonly)
Returns the value of attribute server_info.
20 21 22 |
# File 'lib/mcp/client/http.rb', line 20 def server_info @server_info end |
#session_id ⇒ Object (readonly)
Returns the value of attribute session_id.
20 21 22 |
# File 'lib/mcp/client/http.rb', line 20 def session_id @session_id end |
#url ⇒ Object (readonly)
Returns the value of attribute url.
20 21 22 |
# File 'lib/mcp/client/http.rb', line 20 def url @url end |
Instance Method Details
#close ⇒ Object
Terminates the session by sending an HTTP DELETE to the MCP endpoint with the current ‘Mcp-Session-Id` header, and clears locally tracked session state afterward. No-op when no session has been established.
Per spec, the server MAY respond with HTTP 405 Method Not Allowed when it does not support client-initiated termination, and returns 404 for a session it has already terminated. Both mean the session is gone —the desired end state. Other errors surface to the caller; local session state is cleared either way. modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/mcp/client/http.rb', line 199 def close unless @session_id clear_session return end begin client.delete("", nil, session_headers) rescue Faraday::ClientError => e raise unless [404, 405].include?(e.response&.dig(:status)) ensure clear_session end end |
#connect(client_info: nil, protocol_version: nil, capabilities: {}) ⇒ Hash
Performs the MCP ‘initialize` handshake: sends an `initialize` request followed by the required `notifications/initialized` notification. The server’s ‘InitializeResult` (protocol version, capabilities, server info, instructions) is cached on the transport and returned.
Idempotent: a second call returns the cached ‘InitializeResult` without contacting the server. After `close`, state is cleared and `connect` will handshake again.
modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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 |
# File 'lib/mcp/client/http.rb', line 50 def connect(client_info: nil, protocol_version: nil, capabilities: {}) return @server_info if connected? client_info ||= { name: "mcp-ruby-client", version: MCP::VERSION } protocol_version ||= MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION response = send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, id: SecureRandom.uuid, method: MCP::Methods::INITIALIZE, params: { protocolVersion: protocol_version, capabilities: capabilities, clientInfo: client_info, }, }) if response.is_a?(Hash) && response.key?("error") clear_session error = response["error"] raise RequestHandlerError.new( "Server initialization failed: #{error["message"]}", { method: MCP::Methods::INITIALIZE }, error_type: :internal_error, ) end unless response.is_a?(Hash) && response["result"].is_a?(Hash) clear_session raise RequestHandlerError.new( "Server initialization failed: missing result in response", { method: MCP::Methods::INITIALIZE }, error_type: :internal_error, ) end @server_info = response["result"] negotiated_protocol_version = @server_info["protocolVersion"] unless MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(negotiated_protocol_version) clear_session raise RequestHandlerError.new( "Server initialization failed: unsupported protocol version #{negotiated_protocol_version.inspect}", { method: MCP::Methods::INITIALIZE }, error_type: :internal_error, ) end begin send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, method: MCP::Methods::NOTIFICATIONS_INITIALIZED, }) rescue StandardError clear_session raise end @connected = true @server_info end |
#connected? ⇒ Boolean
Returns true once ‘connect` has completed the full handshake (`initialize` response received and `notifications/initialized` sent). Returns false before the first handshake and after `close`.
114 115 116 |
# File 'lib/mcp/client/http.rb', line 114 def connected? @connected end |
#send_request(request:) ⇒ Object
Sends a JSON-RPC request and returns the parsed response body. After a successful ‘initialize` handshake, the session ID and protocol version returned by the server are captured and automatically included on subsequent requests.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/mcp/client/http.rb', line 122 def send_request(request:) method = request[:method] || request["method"] params = request[:params] || request["params"] response = client.post("", request, session_headers) body = parse_response_body(response, method, params) capture_session_info(method, response, body) body rescue Faraday::BadRequestError => e raise RequestHandlerError.new( "The #{method} request is invalid", { method: method, params: params }, error_type: :bad_request, original_error: e, ) rescue Faraday::UnauthorizedError => e raise RequestHandlerError.new( "You are unauthorized to make #{method} requests", { method: method, params: params }, error_type: :unauthorized, original_error: e, ) rescue Faraday::ForbiddenError => e raise RequestHandlerError.new( "You are forbidden to make #{method} requests", { method: method, params: params }, error_type: :forbidden, original_error: e, ) rescue Faraday::ResourceNotFound => e # Per spec, 404 is the session-expired signal only when the request # actually carried an `Mcp-Session-Id`. A 404 without a session attached # (e.g. wrong URL or a stateless server) surfaces as a generic not-found. # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management if @session_id clear_session raise SessionExpiredError.new( "The #{method} request is not found", { method: method, params: params }, original_error: e, ) else raise RequestHandlerError.new( "The #{method} request is not found", { method: method, params: params }, error_type: :not_found, original_error: e, ) end rescue Faraday::UnprocessableEntityError => e raise RequestHandlerError.new( "The #{method} request is unprocessable", { method: method, params: params }, error_type: :unprocessable_entity, original_error: e, ) rescue Faraday::Error => e # Catch-all raise RequestHandlerError.new( "Internal error handling #{method} request", { method: method, params: params }, error_type: :internal_error, original_error: e, ) end |