## Motivation and Context
The ping utility says the receiver "MUST respond promptly with an empty response",
and a server may treat a missing answer as a stale connection and drop it. Both reference SDKs honor
that on the client side (the TypeScript protocol layer installs an automatic pong,
and the Python session answers `PingRequest` with an empty result), but this client answered
a server ping with Method not found over Streamable HTTP and ignored it entirely over stdio,
leaving long-lived legacy sessions vulnerable to a liveness-checking server.
Over HTTP, an unhandled `ping` is now answered with the empty result in `dispatch_server_request`.
The default deliberately does not live in `@server_request_handlers`: that registry's `any?` opens
the GET listening stream on connect, and a built-in pong must not change listener behavior -
a server can only ping on a stream that is already open. A handler registered via
`on_server_request("ping")` still wins.
Over stdio, the read loop now answers a ping frame inline and keeps waiting for its response.
Frames are told apart by shape rather than id: one carrying `method` is a request or notification,
never the awaited response, so a server-chosen ping id that collides with the awaited one is still answered -
and a request frame can no longer be returned to the caller as if it were the response.
Between requests nothing reads stdout, so a queued ping is answered when the next request starts reading,
which is inherent to the single-threaded stdio client. Other server-to-client requests over stdio stay
unsupported as documented.
The pong is best effort on both transports, in the ways the reference SDKs imply: a pong that cannot be written
over a broken stdin is dropped instead of failing the unrelated request whose response the loop is reading,
and a ping whose id is not a String or a Number - a shape the TypeScript and Python schemas reject outright -
is not answered with a pong: over stdio it is skipped entirely, and over HTTP it falls through to Method not found,
exactly as before this change.
The modern lifecycle removes `ping` altogether (SEP-2575), so this applies to handshake-lifecycle connections only.
## How Has This Been Tested?
New transport tests: over HTTP, a ping delivered on the SSE stream results in a POSTed empty-result response with
the ping's id, and a user-registered `"ping"` handler overrides the default; over stdio, a ping injected while
a response is awaited is answered on stdin and the original response still returns. Both fail without the change
(Method not found / no answer). Further tests cover the best-effort edges: a pong write failure over
a closed stdin leaves the awaited response intact, and a malformed-id ping is skipped over stdio
and answered with Method not found over HTTP. `bundle exec rake` (tests, RuboCop, and conformance baseline) passes.
## Breaking Changes
None. A server ping that previously received Method not found (or nothing) now receives the empty result
the specification requires.
2.1 KiB
| layout | title | nav_order |
|---|---|---|
| default | Ping | 6 |
Ping
The MCP ping utility
allows either side of the connection to verify that the peer is still responsive.
{: .note }
MCP 2026-07-28 removes
pingfrom the protocol (SEP-2575): requests of the modern lifecycle are single POST exchanges whose connection itself signals liveness.#pingsends the request regardless of the negotiated lifecycle, so on a modern connection a conforming server (the Ruby SDK server included) rejects it with-32601Method not found andServerErroris raised; ping is a handshake-lifecycle utility.
MCP::Client exposes ping to send a ping to the server:
client = MCP::Client.new(transport: transport)
client.ping # => {} on success
#ping raises MCP::Client::ServerError when the server returns a JSON-RPC error.
It raises MCP::Client::ValidationError when the response result is missing or
is not a Hash (matching the spec requirement that result be an object).
Transport-level errors (for example, MCP::Client::Stdio's read_timeout: firing)
propagate as exceptions raised by the transport layer.
Answering Server Pings
On handshake-lifecycle connections a server may ping the client the same way, and the client answers automatically with
the empty result - no handler is needed. Registering transport.on_server_request("ping") on MCP::Client::HTTP replaces
the automatic answer.
Over stdio, a ping that arrives while a response is awaited is answered inline; between requests it is answered when
the next request starts reading. The answer is best effort: a pong that cannot be written (for example, over a broken pipe) is
dropped rather than failing the request whose response is being read. Independently of pings, consider setting read_timeout:
on MCP::Client::Stdio, since a server that never answers otherwise holds the read until the process exits.
Server Side
How servers answer ping requests and ping the client themselves is documented on
the server Ping page.