Skip to content

Commit 62eb2fa

Browse files
committed
Handle an Initialize Request Sent Without an Id
## Motivation and Context `StreamableHTTPTransport#handle_post` routes to `handle_initialization` via `initialize_request?`, which matches on the method name alone. An `initialize` message sent without an id (framed as a notification) therefore reaches `handle_initialization`. `Server#init` runs and marks the session initialized, but JSON-RPC emits no response for an id-less request, so `handle_json` returns nil. Two things then went wrong: - `handle_initialization` returned `[200, headers, [nil]]`. A `nil` body element is not a valid Rack response, so the web server raises while serializing it (`NoMethodError` on `nil.bytesize`) and the client gets a malformed response or a 500. - The `if session_id && !server_session.initialized?` cleanup guard was skipped, because the session is marked initialized. The session was retained with a `Mcp-Session-Id` header, so a stream of id-less `initialize` messages accumulates sessions toward the `max_sessions` cap until the reaper reclaims them. `handle_initialization` now treats a nil response as an id-less initialize: it discards the orphaned session and acks with 202, mirroring how `handle_regular_request` already handles a nil response. A well-formed `initialize` request (with an id) is unaffected. ## How Has This Been Tested? A new test in `test/mcp/server/transports/streamable_http_transport_test.rb` posts an `initialize` with no id and asserts a 202 with an empty body, no `Mcp-Session-Id` header, and an empty session map. It fails against the previous code (a 200 with a nil body element and a retained session) and passes now. ## Breaking Changes None. A conforming client sends `initialize` as a request with an id and is unaffected; only the malformed id-less form changes, from a broken 200 to a 202 with no retained session.
1 parent e85f6d0 commit 62eb2fa

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

lib/mcp/server/transports/streamable_http_transport.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,17 @@ def handle_initialization(request, body_string, body)
844844

845845
response = server_session.handle_json(body_string)
846846

847+
# `initialize_request?` matches on the method alone, so an `initialize` sent without
848+
# an id (framed as a notification) reaches here. `Server#init` marks the session initialized,
849+
# but JSON-RPC emits no response for an id-less request, so `response` is nil.
850+
# Returning `[200, ..., [nil]]` would place nil in the Rack body (which the web server cannot serialize),
851+
# and the session would be retained since it is marked initialized. Discard the orphaned session
852+
# and ack with 202, mirroring the nil-response handling in a regular request.
853+
if response.nil?
854+
cleanup_session(session_id) if session_id
855+
return handle_accepted
856+
end
857+
847858
# If `Server#init` produced an error response (e.g., malformed JSON-RPC envelope),
848859
# `mark_initialized!` was never called. Discard the orphaned session and omit
849860
# the `Mcp-Session-Id` header so the client retries from a clean state instead of

test/mcp/server/transports/streamable_http_transport_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,24 @@ def string
315315
assert_equal({}, @transport.instance_variable_get(:@sessions))
316316
end
317317

318+
test "acks an initialize sent without an id and retains no session" do
319+
# `initialize` framed as a notification (no id) produces no JSON-RPC response.
320+
# The transport must not put nil in the Rack body or retain the orphaned session.
321+
request = create_rack_request(
322+
"POST",
323+
"/",
324+
{ "CONTENT_TYPE" => "application/json" },
325+
{ jsonrpc: "2.0", method: "initialize" }.to_json,
326+
)
327+
328+
response = @transport.handle_request(request)
329+
330+
assert_equal 202, response[0]
331+
refute response[1].key?("Mcp-Session-Id"), "no session id should leak from an id-less init"
332+
assert_equal [], response[2]
333+
assert_equal({}, @transport.instance_variable_get(:@sessions))
334+
end
335+
318336
test "rejects non-Hash JSON-RPC body with HTTP 400 and -32600" do
319337
request = create_rack_request(
320338
"POST",

0 commit comments

Comments
 (0)