Class: Ask::MCP::Transport::StreamableHTTP
- Inherits:
-
Object
- Object
- Ask::MCP::Transport::StreamableHTTP
- 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
-
#protocol_version ⇒ Object
Set by the client after negotiation; sent as MCP-Protocol-Version.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Instance Method Summary collapse
- #close_listen ⇒ Object
-
#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?...?=.
-
#initialize(url, options = {}) ⇒ StreamableHTTP
constructor
A new instance of StreamableHTTP.
-
#listen(notifications) ⇒ Object
Open a long-lived notification stream via subscriptions/listen (2026-07-28).
- #on_message(&block) ⇒ Object
- #running? ⇒ Boolean
-
#send(message, extra_headers = {}) ⇒ Object
Send a JSON-RPC message.
- #shutdown ⇒ Object
- #start ⇒ Object
- #stop ⇒ Object
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, = {}) @url = url @options = @running = false @message_handlers = [] @http = nil @protocol_version = nil @listen_thread = nil end |
Instance Attribute Details
#protocol_version ⇒ Object
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 |
#url ⇒ Object (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_listen ⇒ Object
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 (&block) @message_handlers << block end |
#running? ⇒ 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(, extra_headers = {}) data = .is_a?(String) ? : .to_json headers = request_headers().merge(extra_headers) response = @http.post(@url, body: data, headers: headers) handle_response(response) rescue HTTPX::Error => e raise ConnectionError, "HTTP error: #{e.}" end |
#shutdown ⇒ Object
108 109 110 |
# File 'lib/ask/mcp/transport/streamable_http.rb', line 108 def shutdown stop end |
#start ⇒ Object
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 |
#stop ⇒ Object
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 |