## 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.
45 lines
2.1 KiB
Markdown
45 lines
2.1 KiB
Markdown
---
|
|
layout: default
|
|
title: Ping
|
|
nav_order: 6
|
|
---
|
|
|
|
# Ping
|
|
|
|
The [MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping)
|
|
allows either side of the connection to verify that the peer is still responsive.
|
|
|
|
{: .note }
|
|
> MCP 2026-07-28 removes `ping` from the protocol (SEP-2575): requests of
|
|
> the [modern lifecycle](/client/lifecycle/) are single POST exchanges whose connection itself
|
|
> signals liveness. `#ping` sends the request regardless of the negotiated lifecycle,
|
|
> so on a modern connection a conforming server (the Ruby SDK server included) rejects it
|
|
> with `-32601` Method not found and `ServerError` is raised; ping is a handshake-lifecycle utility.
|
|
|
|
`MCP::Client` exposes `ping` to send a ping to the server:
|
|
|
|
```ruby
|
|
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](/server/ping/) page.
|