Fix client methods silently swallowing JSON-RPC error responses

## Motivation and Context

Client methods (`tools`, `resources`, `resource_templates`, `prompts`, `read_resource`, `get_prompt`, `call_tool`)
silently swallowed JSON-RPC error responses by falling back to empty defaults. This did not conform to JSON-RPC 2.0,
which clearly distinguishes error responses from success responses.
Both the Python SDK (`MCPError`) and TypeScript SDK (`ProtocolError`) raise exceptions on error responses.
The Ruby SDK was the only one silently discarding them.

Introduces a common `request` method that handles JSON-RPC request construction and error checking in one place,
replacing the duplicated `transport.send_request` calls across all client methods.

## How Has This Been Tested?

Added tests for each affected method verifying that `ServerError` is raised with the correct `code`, `message`,
and `data` attributes.

## Breaking Changes

The previous behavior of silently returning empty defaults on server errors did not conform to JSON-RPC 2.0.
Error responses should be surfaced to callers, not swallowed.
Callers that relied on the silent fallback behavior will need to rescue `MCP::Client::ServerError`.
This commit is contained in:
Koichi ITO 2026-03-31 12:01:18 +09:00
commit 590feb13a5

View file

@ -369,5 +369,92 @@ module MCP
client = Client.new(transport: transport)
client.call_tool(tool: tool, arguments: arguments)
end
def test_tools_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_601, "message" => "Method not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.tools }
assert_equal(-32_601, error.code)
assert_equal("Method not found", error.message)
end
def test_resources_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Invalid params" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.resources }
assert_equal(-32_602, error.code)
end
def test_read_resource_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Resource not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
assert_raises(Client::ServerError) { client.read_resource(uri: "file:///missing") }
end
def test_get_prompt_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Prompt not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
assert_raises(Client::ServerError) { client.get_prompt(name: "missing") }
end
def test_prompts_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_601, "message" => "Method not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
assert_raises(Client::ServerError) { client.prompts }
end
def test_resource_templates_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_601, "message" => "Method not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
assert_raises(Client::ServerError) { client.resource_templates }
end
def test_call_tool_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Tool not found" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.call_tool(name: "missing") }
assert_equal(-32_602, error.code)
end
def test_server_error_includes_data_field
transport = mock
mock_response = {
"error" => { "code" => -32_603, "message" => "Internal error", "data" => "extra details" },
}
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.tools }
assert_equal("extra details", error.data)
end
end
end