Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/ruby_llm/mcp/native/transports/streamable_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ def handle_success_response(response, request_id, _original_message)
start_sse_stream if sse_fallback_available?
nil
elsif content_type&.include?("application/json")
# Notifications have no request_id and nothing waits on their response. The spec says
# servers respond to them with 202 and no body, but some return 200 with `body: { ok: true }`
# that is not a JSON-RPC envelope, but still a success response.
return if request_id.nil?

response_body = response.respond_to?(:body) ? response.body.to_s : "{}"
if response_body == "null" # Fix related to official MCP Ruby SDK implementation
response_body = "{}"
Expand All @@ -447,11 +452,9 @@ def handle_success_response(response, request_id, _original_message)
json_response = parse_and_validate_http_response(response_body)
result = RubyLLM::MCP::Result.new(json_response, session_id: @session_id)

if request_id
@pending_mutex.synchronize do
queue = @pending_requests.delete(request_id.to_s)
queue&.push(result)
end
@pending_mutex.synchronize do
queue = @pending_requests.delete(request_id.to_s)
queue&.push(result)
end

result
Expand Down
35 changes: 33 additions & 2 deletions spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ def handle(body)
end
end

describe "#handle_success_response" do
let(:response) { instance_double(HTTPX::Response) }

before do
allow(response).to receive(:respond_to?).with(:headers).and_return(true)
allow(response).to receive(:respond_to?).with(:body).and_return(true)
allow(response).to receive_messages(
headers: { "content-type" => "application/json" },
body: '{"ok":true}'
)
end

it "ignores a non-envelope JSON body in the response to a notification" do
result = transport.send(
:handle_success_response, response, nil, { "method" => "notifications/initialized" }
)

expect(result).to be_nil
end

it "still validates the envelope in the response to a request" do
expect do
transport.send(
:handle_success_response, response, 1, { "method" => "tools/list", "id" => 1 }
)
end.to raise_error(RubyLLM::MCP::Errors::TransportError, /Invalid JSON-RPC envelope/)
end
end

describe "protocol version negotiation" do
it "successfully initializes and negotiates protocol version" do
client.start
Expand Down Expand Up @@ -791,9 +820,11 @@ def handle(body)
}.to_json
)

# Request without ID should be handled properly (notification)
# A request without ID is a notification: the response body is ignored,
# but the session id header is still captured
result = transport.request({ "method" => "test" }, wait_for_response: false)
expect(result.session_id).to eq(session_id)
expect(result).to be_nil
expect(transport.session_id).to eq(session_id)
end

it "handles very large response gracefully" do
Expand Down
Loading