Skip to content

Commit baa093d

Browse files
authored
Merge pull request #324 from koic/ping_client_to_server
Support `ping` client API per MCP specification
2 parents cbe8145 + 493321c commit baa093d

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,33 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
10991099
- `notifications/progress`
11001100
- `notifications/message`
11011101

1102+
### Ping
1103+
1104+
The MCP Ruby SDK supports the
1105+
[MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping),
1106+
which allows either side of the connection to verify that the peer is still responsive.
1107+
A `ping` request has no parameters, and the receiver MUST respond promptly with an empty result.
1108+
1109+
#### Server-Side
1110+
1111+
Servers respond to incoming `ping` requests automatically - no setup is required.
1112+
Any `MCP::Server` instance replies with an empty result.
1113+
1114+
#### Client-Side
1115+
1116+
`MCP::Client` exposes `ping` to send a ping to the server:
1117+
1118+
```ruby
1119+
client = MCP::Client.new(transport: transport)
1120+
client.ping # => {} on success
1121+
```
1122+
1123+
`#ping` raises `MCP::Client::ServerError` when the server returns a JSON-RPC error.
1124+
It raises `MCP::Client::ValidationError` when the response `result` is missing or
1125+
is not a Hash (matching the spec requirement that `result` be an object).
1126+
Transport-level errors (for example, `MCP::Client::Stdio`'s `read_timeout:` firing)
1127+
propagate as exceptions raised by the transport layer.
1128+
11021129
### Progress
11031130

11041131
The MCP Ruby SDK supports progress tracking for long-running tool operations,
@@ -1549,6 +1576,7 @@ The `MCP::Client` class provides an interface for interacting with MCP servers.
15491576

15501577
This class supports:
15511578

1579+
- Liveness check via the `ping` method (`MCP::Client#ping`)
15521580
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
15531581
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
15541582
- Resource listing via the `resources/list` method (`MCP::Client#resources`)

lib/mcp/client.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ def initialize(message, request, error_type: :internal_error, original_error: ni
2828
end
2929
end
3030

31+
# Raised when a server response fails client-side validation, e.g., a success response
32+
# whose `result` field is missing or has the wrong type. This is distinct from a
33+
# server-returned JSON-RPC error, which is raised as `ServerError`.
34+
class ValidationError < StandardError; end
35+
3136
# Initializes a new MCP::Client instance.
3237
#
3338
# @param transport [Object] The transport object to use for communication with the server.
@@ -303,6 +308,24 @@ def complete(ref:, argument:, context: nil)
303308
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
304309
end
305310

311+
# Sends a `ping` request to the server to verify the connection is alive.
312+
# Per the MCP spec, the server responds with an empty result.
313+
#
314+
# @return [Hash] An empty hash on success.
315+
# @raise [ServerError] If the server returns a JSON-RPC error.
316+
# @raise [ValidationError] If the response `result` is missing or not a Hash.
317+
#
318+
# @example
319+
# client.ping # => {}
320+
#
321+
# @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
322+
def ping
323+
result = request(method: Methods::PING)["result"]
324+
raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
325+
326+
result
327+
end
328+
306329
private
307330

308331
def request(method:, params: nil)

test/mcp/client_test.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,67 @@ def test_complete_returns_default_when_result_is_missing
538538
refute(result["hasMore"])
539539
end
540540

541+
def test_ping_sends_request_and_returns_empty_hash
542+
transport = mock
543+
mock_response = { "result" => {} }
544+
545+
transport.expects(:send_request).with do |args|
546+
req = args[:request]
547+
req[:method] == "ping" &&
548+
req[:jsonrpc] == "2.0" &&
549+
!req.key?(:params)
550+
end.returns(mock_response).once
551+
552+
client = Client.new(transport: transport)
553+
result = client.ping
554+
555+
assert_equal({}, result)
556+
end
557+
558+
def test_ping_raises_server_error_on_error_response
559+
transport = mock
560+
mock_response = { "error" => { "code" => -32_603, "message" => "Internal error" } }
561+
562+
transport.expects(:send_request).returns(mock_response).once
563+
564+
client = Client.new(transport: transport)
565+
error = assert_raises(Client::ServerError) { client.ping }
566+
assert_equal(-32_603, error.code)
567+
assert_equal("Internal error", error.message)
568+
end
569+
570+
def test_ping_raises_validation_error_when_result_is_missing
571+
transport = mock
572+
mock_response = {}
573+
574+
transport.expects(:send_request).returns(mock_response).once
575+
576+
client = Client.new(transport: transport)
577+
error = assert_raises(Client::ValidationError) { client.ping }
578+
assert_equal("Response validation failed: missing or invalid `result`", error.message)
579+
end
580+
581+
def test_ping_raises_validation_error_when_result_is_wrong_type
582+
transport = mock
583+
mock_response = { "result" => "ok" }
584+
585+
transport.expects(:send_request).returns(mock_response).once
586+
587+
client = Client.new(transport: transport)
588+
error = assert_raises(Client::ValidationError) { client.ping }
589+
assert_equal("Response validation failed: missing or invalid `result`", error.message)
590+
end
591+
592+
def test_ping_propagates_transport_errors
593+
transport = mock
594+
transport_error = StandardError.new("read timeout")
595+
transport.expects(:send_request).raises(transport_error).once
596+
597+
client = Client.new(transport: transport)
598+
error = assert_raises(StandardError) { client.ping }
599+
assert_equal("read timeout", error.message)
600+
end
601+
541602
def test_tools_auto_paginates_across_multiple_pages
542603
transport = mock
543604

0 commit comments

Comments
 (0)