Support ping client API per MCP specification
## Motivation and Context The MCP specification defines the `ping` utility for verifying that the peer of a connection is still responsive. The Ruby SDK server side already replies to incoming `ping` requests with an empty result, but `MCP::Client` had no public API to send a `ping` request. Users had to build the JSON-RPC payload themselves and call the transport directly. This adds a client-side counterpart to the existing server handler and aligns the Ruby SDK in spirit with the Python and TypeScript SDKs, which expose `ClientSession.send_ping` and `Client.ping` respectively. Those SDKs validate the response envelope via type schemas (`EmptyResult` / `EmptyResultSchema`) and raise their validation library's generic exception (Pydantic's `ValidationError` / Zod's `ZodError`). The Ruby client applies a Hash type check on `result` (matching Python's behavior of requiring a dict while allowing arbitrary contents) and raises a new `MCP::Client::ValidationError` for malformed responses, keeping `MCP::Client::ServerError` reserved for JSON-RPC error responses. ## How Has This Been Tested? Client tests cover: - request structure (method is `ping`, no `params`) - empty result on success - `ServerError` on JSON-RPC error response - `ValidationError` on a response missing `result` - `ValidationError` on a `result` of the wrong type (e.g., String) - propagation of transport-level errors raised by `transport.send_request` The existing server-side `ping` handler tests continue to pass unchanged. ## Breaking Changes None. `MCP::Client#ping` and `MCP::Client::ValidationError` are purely additive. The server-side `ping` handler behavior is unchanged.
This commit is contained in:
parent
2edba778f0
commit
493321ca46
1 changed files with 112 additions and 0 deletions
|
|
@ -538,6 +538,67 @@ module MCP
|
|||
refute(result["hasMore"])
|
||||
end
|
||||
|
||||
def test_ping_sends_request_and_returns_empty_hash
|
||||
transport = mock
|
||||
mock_response = { "result" => {} }
|
||||
|
||||
transport.expects(:send_request).with do |args|
|
||||
req = args[:request]
|
||||
req[:method] == "ping" &&
|
||||
req[:jsonrpc] == "2.0" &&
|
||||
!req.key?(:params)
|
||||
end.returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
result = client.ping
|
||||
|
||||
assert_equal({}, result)
|
||||
end
|
||||
|
||||
def test_ping_raises_server_error_on_error_response
|
||||
transport = mock
|
||||
mock_response = { "error" => { "code" => -32_603, "message" => "Internal error" } }
|
||||
|
||||
transport.expects(:send_request).returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
error = assert_raises(Client::ServerError) { client.ping }
|
||||
assert_equal(-32_603, error.code)
|
||||
assert_equal("Internal error", error.message)
|
||||
end
|
||||
|
||||
def test_ping_raises_validation_error_when_result_is_missing
|
||||
transport = mock
|
||||
mock_response = {}
|
||||
|
||||
transport.expects(:send_request).returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
error = assert_raises(Client::ValidationError) { client.ping }
|
||||
assert_equal("Response validation failed: missing or invalid `result`", error.message)
|
||||
end
|
||||
|
||||
def test_ping_raises_validation_error_when_result_is_wrong_type
|
||||
transport = mock
|
||||
mock_response = { "result" => "ok" }
|
||||
|
||||
transport.expects(:send_request).returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
error = assert_raises(Client::ValidationError) { client.ping }
|
||||
assert_equal("Response validation failed: missing or invalid `result`", error.message)
|
||||
end
|
||||
|
||||
def test_ping_propagates_transport_errors
|
||||
transport = mock
|
||||
transport_error = StandardError.new("read timeout")
|
||||
transport.expects(:send_request).raises(transport_error).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
error = assert_raises(StandardError) { client.ping }
|
||||
assert_equal("read timeout", error.message)
|
||||
end
|
||||
|
||||
def test_tools_auto_paginates_across_multiple_pages
|
||||
transport = mock
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue