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
8 changes: 6 additions & 2 deletions lib/mcp/client/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def send_request(request:)
nil
end

body = resolve_response_body(stream, response, method, params)
body = resolve_response_body(stream, response, method, params, notification: !(request[:id] || request["id"]))

# Every way a response arrives (JSON body, SSE event, resumed stream) ends here. Checked before
# anything is learned from the body, and only for a request: a notification awaits no response.
Expand Down Expand Up @@ -1107,7 +1107,7 @@ def require_faraday!

# Determines the logical JSON-RPC body of the exchange after the POST stream finished
# (or was aborted because the response already arrived).
def resolve_response_body(stream, response, method, params)
def resolve_response_body(stream, response, method, params, notification: false)
return stream.response if stream.response

# 202 Accepted is the server's ACK for a JSON-RPC notification or response; no body is expected.
Expand Down Expand Up @@ -1141,6 +1141,10 @@ def resolve_response_body(stream, response, method, params)
# added by the Faraday customizer may have parsed it already.
response.body.is_a?(String) ? parse_json_buffer(response.body, method, params) : response.body
else
# A notification awaits no response, so a server that acknowledges it with 200 and no body
# instead of 202 has still accepted it. The TypeScript and Python SDKs also accept this.
return if notification && response.success?

raise RequestHandlerError.new(
"Unsupported Content-Type: #{content_type.inspect}. Expected application/json or text/event-stream.",
{ method: method, params: params },
Expand Down
77 changes: 77 additions & 0 deletions test/mcp/client/http_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,58 @@ def test_send_request_returns_nil_for_202_accepted_response
assert_nil(response)
end

def test_send_request_returns_nil_for_notification_acknowledged_with_empty_200
request = {
jsonrpc: "2.0",
method: "notifications/initialized",
}

stub_request(:post, url)
.with(body: request.to_json)
.to_return(status: 200, body: "")

response = client.send_request(request: request)

assert_nil(response)
end

def test_send_request_raises_error_for_notification_answered_with_empty_4xx_or_5xx
request = {
jsonrpc: "2.0",
method: "notifications/initialized",
}

[400, 500].each do |status|
stub_request(:post, url)
.with(body: request.to_json)
.to_return(status: status, body: "")

error = assert_raises(RequestHandlerError) do
client.send_request(request: request)
end

assert_equal(status, error.original_error.response[:status])
end
end

def test_send_request_raises_error_for_request_answered_with_empty_200
request = {
jsonrpc: "2.0",
id: "test_id",
method: "tools/list",
}

stub_request(:post, url)
.with(body: request.to_json)
.to_return(status: 200, body: "")

error = assert_raises(RequestHandlerError) do
client.send_request(request: request)
end

assert_equal(:unsupported_media_type, error.error_type)
end

def test_send_request_raises_error_for_sse_without_response
request = {
jsonrpc: "2.0",
Expand Down Expand Up @@ -2249,6 +2301,31 @@ def test_connect_performs_initialize_handshake
assert_equal({ "name" => "test-server", "version" => "1.0" }, result["serverInfo"])
end

def test_connect_accepts_initialized_notification_acknowledged_with_empty_200
stub_request(:post, url)
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: {
jsonrpc: "2.0",
result: {
protocolVersion: "2025-11-25",
capabilities: {},
serverInfo: { name: "test-server", version: "1.0" },
},
}.to_json,
)
stub_request(:post, url)
.with { |req| JSON.parse(req.body)["method"] == "notifications/initialized" }
.to_return(status: 200, body: "")

result = client.connect

assert_predicate(client, :connected?)
assert_equal("2025-11-25", result["protocolVersion"])
end

def test_connect_caches_server_info
stub_initialize
stub_notification
Expand Down