Class: Ask::MCP::Transport::StreamableHTTP

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/mcp/transport/streamable_http.rb

Overview

Streamable HTTP transport (2026-07-28 shape).

Every JSON-RPC message is its own HTTP POST to the single MCP endpoint. Each POST carries the mirrored request-metadata headers:

- MCP-Protocol-Version (matching the body's `_meta` protocolVersion)
- Mcp-Method (the JSON-RPC method)
- Mcp-Name (params.name / params.uri for tools/call, resources/read,
prompts/get)

The server answers with either a single application/json object or an SSE stream scoped to the request; the client must handle both, chosen per response by Content-Type.

Protocol-level sessions, the GET stream endpoint, and Last-Event-ID resumability were removed in 2026-07-28 and are not implemented.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, options = {}) ⇒ StreamableHTTP

Returns a new instance of StreamableHTTP.



27
28
29
30
31
32
33
34
35
# File 'lib/ask/mcp/transport/streamable_http.rb', line 27

def initialize(url, options = {})
  @url = url
  @options = options
  @running = false
  @message_handlers = []
  @http = nil
  @protocol_version = nil
  @listen_thread = nil
end

Instance Attribute Details

#protocol_versionObject

Set by the client after negotiation; sent as MCP-Protocol-Version.



25
26
27
# File 'lib/ask/mcp/transport/streamable_http.rb', line 25

def protocol_version
  @protocol_version
end

#urlObject (readonly)

Returns the value of attribute url.



23
24
25
# File 'lib/ask/mcp/transport/streamable_http.rb', line 23

def url
  @url
end

Instance Method Details

#close_listenObject



99
100
101
102
# File 'lib/ask/mcp/transport/streamable_http.rb', line 99

def close_listen
  @listen_thread&.kill
  @listen_thread = nil
end

#encode_header_value(value) ⇒ Object

Encode a value for use as an HTTP header value per the spec: plain visible ASCII passes through; anything else (non-ASCII, control characters, leading/trailing whitespace, or a value matching the Base64 sentinel pattern) is encoded as =?base64?...?=.



116
117
118
119
# File 'lib/ask/mcp/transport/streamable_http.rb', line 116

def encode_header_value(value)
  str = value.to_s
  header_safe?(str) ? str : "=?base64?#{Base64.strict_encode64(str)}?="
end

#listen(notifications) ⇒ Object

Open a long-lived notification stream via subscriptions/listen (2026-07-28). The response SSE stream stays open; delivered notifications (e.g. notifications/tools/list_changed, notifications/resources/updated) are passed to on_message handlers. The notifications filter is a Hash like { toolsListChanged: true, resourceSubscriptions: ["uri"] } Close the stream by calling #close_listen or #stop.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ask/mcp/transport/streamable_http.rb', line 80

def listen(notifications)
  require "httpx"

  request = Native::Messages::Request.new(
    method: "subscriptions/listen",
    params: { notifications: notifications },
    id: @options[:listen_id] || 1
  )
  headers = request_headers(request)
  response = @http.post(@url, body: request.to_json, headers: headers)

  unless response.status == 200
    raise ConnectionError, "HTTP #{response.status}: #{response.body.to_s[0..200]}"
  end

  @listen_thread = Thread.new { read_sse_stream(response) }
  self
end

#on_message(&block) ⇒ Object



37
38
39
# File 'lib/ask/mcp/transport/streamable_http.rb', line 37

def on_message(&block)
  @message_handlers << block
end

#running?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/ask/mcp/transport/streamable_http.rb', line 104

def running?
  @running
end

#send(message, extra_headers = {}) ⇒ Object

Send a JSON-RPC message. extra_headers (e.g. Mcp-Param-* mirrored from tool parameters) are merged into the request metadata headers.



64
65
66
67
68
69
70
71
# File 'lib/ask/mcp/transport/streamable_http.rb', line 64

def send(message, extra_headers = {})
  data = message.is_a?(String) ? message : message.to_json
  headers = request_headers(message).merge(extra_headers)
  response = @http.post(@url, body: data, headers: headers)
  handle_response(response)
rescue HTTPX::Error => e
  raise ConnectionError, "HTTP error: #{e.message}"
end

#shutdownObject



108
109
110
# File 'lib/ask/mcp/transport/streamable_http.rb', line 108

def shutdown
  stop
end

#startObject



41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/ask/mcp/transport/streamable_http.rb', line 41

def start
  require "httpx"

  headers = { "Content-Type" => "application/json" }
  headers["Accept"] = "application/json, text/event-stream"
  headers.merge!(@options[:headers]) if @options[:headers]

  @http = HTTPX.with(
    headers:,
    timeout: { request_timeout: @options[:timeout] || 30 }
  )
  @running = true
  self
end

#stopObject



56
57
58
59
60
# File 'lib/ask/mcp/transport/streamable_http.rb', line 56

def stop
  @running = false
  @listen_thread&.kill
  @http&.close
end