Class: MCP::Client::HTTP

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(url:, headers: {}, &block) ⇒ HTTP

Returns a new instance of HTTP.



18
19
20
21
22
23
24
# File 'lib/mcp/client/http.rb', line 18

def initialize(url:, headers: {}, &block)
  @url = url
  @headers = headers
  @faraday_customizer = block
  @session_id = nil
  @protocol_version = nil
end

Instance Attribute Details

#protocol_versionObject (readonly)

Returns the value of attribute protocol_version.



16
17
18
# File 'lib/mcp/client/http.rb', line 16

def protocol_version
  @protocol_version
end

#session_idObject (readonly)

Returns the value of attribute session_id.



16
17
18
# File 'lib/mcp/client/http.rb', line 16

def session_id
  @session_id
end

#urlObject (readonly)

Returns the value of attribute url.



16
17
18
# File 'lib/mcp/client/http.rb', line 16

def url
  @url
end

Instance Method Details

#closeObject

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



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/mcp/client/http.rb', line 107

def close
  return unless @session_id

  begin
    client.delete("", nil, session_headers)
  rescue Faraday::ClientError => e
    raise unless [404, 405].include?(e.response&.dig(:status))
  ensure
    clear_session
  end
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.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
# File 'lib/mcp/client/http.rb', line 30

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