Skip to content

fix(mcpl): answer a server's MCP ping, on a connected transport and during the handshake (#392) - #502

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/392-mcp-ping
Oct 18, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/392-mcp-ping

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

The host now answers an MCPL server's MCP ping with an empty result: on a connected transport, while its planes are paused too, and during the initialize handshake. A ping with id 0 during the handshake is no longer taken for the initialize response. !Fixes #392.

Problem

  • No answer to ping. MCP lets either side send ping, and the receiver must answer promptly with an empty result. METHOD_TO_EVENT has no ping, and nothing else in src/mcpl/ answers one. So on main, since fix(mcpl): contain inbound request handler failures #208, a server's ping is answered -32601 "Method not found". By our reading of 0.22.2's code, that release leaves a ping unanswered. A server whose keepalive treats a failed ping as a dead peer may disconnect.
  • The handshake. MCP 2024-11-05, the revision this host speaks, lets a server ping before it receives initialized. The handshake reader matches the initialize response by its id alone, and the host gives initialize the id 0, which is also the first id of a server whose own request ids start at 0. So a server that pings with id 0 as initialize arrives has its ping taken for the initialize response. The host connects as if the server had advertised no MCPL, and the server is still waiting for its pong.

Changes

  • dispatchMessage answers a ping with {} as it arrives, before admission and before buffering. A ping sent as a notification has nothing to answer, and is ignored.
  • The handshake reader skips anything that carries a method, since a request or a notification is never the initialize response, and answers a ping there with {}. Anything else a server sends before its initialize response is still ignored, as before.
  • Changelog fragment 392-mcp-ping.fixed.md.

What each part is for

  • The router's answer is the prompt answer MCP asks for, which a server's keepalive relies on. It answers while a plane is paused for a startup, a reconnect, an awareness drain or a quiesce, since a ping asks only whether the connection is alive. Without it, a ping gets -32601. Mapping ping to an event for the framework to answer would put the answer behind admission and buffering, so a paused plane would hold it. It costs one branch in the router.
  • The handshake's skip keeps any request or notification from being read as the initialize response, whatever its id. Without it, a server's request with id 0 is taken for it. Giving initialize an id no server is likely to use would only make that collision less likely. It costs one check.
  • The handshake's answer lets a server that waits for its pong before it answers initialize connect. Without it, that server fails the handshake at its 30 s timeout. It's a second place that answers a ping, because the connection's router isn't attached until the handshake ends.

What a deployment will notice

  • A server that pings the host gets {}, where it gets -32601 on main and, by our reading, got no answer in 0.22.2.
  • A server that pings with id 0 during the handshake connects with the capabilities it advertised, where the host took it for a server with no MCPL.
  • A server's request with id 0 other than a ping, sent before its initialize response, now goes unanswered, as one with any other id did before. The host used to take it for the response and connect as if the server had advertised no MCPL. By our reading, a server that waits for that answer before it answers initialize now fails the handshake at its 30 s timeout, as the pings-first test's server does when the handshake's ping answer is taken out (Tests). MCP 2024-11-05 says a server SHOULD NOT send requests other than pings and logging before it receives initialized.

Tests

test/mcpl-ping.test.ts, on the connection's real routing and response encoding:

  • a ping on a ready connection is answered {} for the ids 0, 1 and "p-2", and a ping notification gets no answer;
  • a ping while both planes are closed is answered at once, and nothing is buffered for it;
  • a stdio server pings with id 0 as soon as initialize arrives, and answers initialize only once its pong arrives. The host connects with the server's MCPL capabilities, and the only response the server got is that pong;
  • a stdio server writes in one chunk, as initialize arrives: a roots/list request with id 0, a request whose method is 5, a ping sent as a notification, a log notification, its initialize response, and a ping with the id "late". The host connects with the server's MCPL capabilities, and the only response the server gets is the pong to "late", which the connected transport answers after the handshake.

Runs at 8baec31b, on Linux with Node 24.22.0:

  • tsc --noEmit is clean, and test/mcpl-ping.test.ts passes 4 of 4.

  • Red-first. With main's src/mcpl/server-connection.ts at 0d19f7c8 and these tests, all 4 fail. The two connected-transport tests get -32601. In the pings-first test the server never gets its pong, and in the one-chunk test the host connects without the server's capabilities.

  • Each piece. Taking out each of five pieces on its own fails the test aimed at it:

    Piece taken out Caught by
    the answer in dispatchMessage both connected-transport tests, which get -32601, and the one-chunk test, whose late ping does too
    the answer in the handshake the pings-first test, at the handshake's 30 s timeout
    the handshake's check for an id, which leaves a ping notification unanswered the one-chunk test, whose server gets a response with no id
    skipping every message that carries a method, narrowed to ping the one-chunk test, where roots/list with id 0 is taken for the response
    the return after the handshake's ping answer both handshake tests

CI runs the typecheck, the build and the suite on this pull request.

Not verified

  • No real MCPL server. The servers here are scripts written for these tests, and we ran no server in the ecosystem against this.
  • The host sends no pings of its own. MCP lets either side send one, and this only answers the server's.

  • Changelog fragment added — changelog.d/392-mcp-ping.fixed.md

Written by Tabitha-2696 (Claude Opus 5.5), contributing through nissa-seru. Ada-1017 made the design calls. Our internal review read the first commit, and its findings are in the second. Ramona-2416 (Claude Opus 5.5) reviewed it, and read it again after its rebase onto main.

Description checked and posted by Lottie-2661 (Claude Opus 5.5).

🤖 Written with Claude Opus 5.5 through the Rookery's Pi tooling.

Tabitha-2696 and others added 2 commits October 18, 2026 05:14
…uring the handshake (anima-research#392)

MCP's ping lets either side ask whether its peer is alive, and the
receiver must answer promptly with an empty result. The host answered a
server's ping nowhere. On a connected transport the method had no entry
in METHOD_TO_EVENT, so it went unanswered before anima-research#208 and gets -32601
"Method not found" with it; a server whose keepalive treats a failed ping
as a dead peer may disconnect.

dispatchMessage now answers a ping with {} as it arrives, before
admission and before buffering: it asks only whether the connection is
alive, and a plane paused for startup, a reconnect, an awareness drain or
a quiesce mustn't hold that answer. A ping sent as a notification has
nothing to answer and is ignored.

The handshake reader is the other place the host reads a server, and MCP
2024-11-05, the revision this host speaks, lets a server ping before it
receives `initialized`. The reader matched the initialize response by id
alone, and gave initialize id 0, the id a server whose own request ids
start at 0 gives its first request. Probed on main 91e70ed: a server
that pinged with id 0 on receiving initialize had the ping taken for the
initialize response, so the host connected with capabilities null (no
MCPL) while the server still waited for its pong. The reader now skips
anything that carries a method, since a request or a notification is
never the response, and answers a ping there with {}. Anything else a
server sends during the handshake is still ignored, as before.

Tests (test/mcpl-ping.test.ts), on the connection's real routing and
response encoding:
- a ping on a ready connection is answered {} for a number and a string
  id, and a ping notification gets nothing;
- a ping while both planes are closed is answered at once, and nothing is
  buffered for it;
- a stdio server that pings with id 0 on receiving initialize, and
  answers initialize once its pong arrives (or after 2 s without one,
  saying so on stderr): the host connects with the server's MCPL
  capabilities, and the server got its pong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wait on events, not a timer (anima-research#392)

Our internal review of the first commit found these, each fixed here.

The only handshake test sent a ping, so narrowing the handshake's skip
to ping, or dropping its guard for a ping with no id, passed every test,
and nothing ran a ping held across the handoff to the router. A second
handshake server writes in one chunk, as initialize arrives: a
roots/list request with id 0, a request whose method is 5, a ping
notification, a log notification, its initialize response and a ping
with the id "late". The host must connect with the server's MCPL
capabilities, and the only response the server gets is the pong to
"late", which the connected transport answers after the handshake.

The first handshake server answered initialize after its pong or after
2 s without one, so a slow child could report pong:no for a pong the
host sent at once. It now answers only once its pong arrives; a lost
pong fails at the handshake's own 30 s timeout. Both servers report
every response they got, by id and kind, on stderr, and the test's wait
ends at that line, with 10 s as its outer bound.

The connected-transport test pings with id 0 too.

The changelog names 0.22.x as the release where a ping went unanswered,
and says it was any request with id 0, not only a ping, that the
handshake took for the initialize response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 18, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit 918e491 into anima-research:main Oct 18, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…eps its class, outcome and data, with W4a's words

main moved from 0d19f7c to 6dfea06 (trains K and L: anima-research#445, anima-research#494, anima-research#496,
anima-research#497, anima-research#499, anima-research#500, anima-research#502, anima-research#503, anima-research#504, anima-research#505, anima-research#247, anima-research#507, anima-research#509). Only
anima-research#247 conflicts, in server-connection.ts:
five rejections it wraps in McplRequestError (this host's close and a
drop with a request pending, and a timeout: no-response; a request on a
closed connection: not-sent; an error answer: error-response with the
JSON-RPC error's data). Each keeps anima-research#247's class, outcome and data, with
W4a's words as its message (resolve247.py in my notes; the error answer's
rejection, after the conflict, takes the class too).

Beyond the conflicts, two calls Ada made (room-203 #142751):
- anima-research#247's synchronous write rejection read `Cannot send <method> to MCPL
  server "<s>": <transport error>`, a request method in the agent's text.
  It now reads "The request wasn't sent: this host couldn't write it to the
  "<s>" server, so nothing ran. Try again later.", and the method, id and
  the transport's words go to a log line (logDetail).
- publishErrorOutcome composes `Publish to <c> failed: ${reason}. Nothing
  was posted.` and `${message} (delivery uncertain)`, and W4a's messages are
  sentences, so one final period comes off this host's own words for a
  request not sent or not answered. A server's error answer keeps its words
  as they came, as anima-research#247's test of a partial post's account requires.

Tests from main that pinned the texts W4a changes take W4a's words:
mcpl-request-outcome (error answer, timeout, channels/open's deadline,
closed connection, the write rejection with its log line),
mcpl-request-timeout (channels/publish's deadline), prose-drafts (the
unconfirmed marker), mcpl-late-render (push/render's timeout, and a
tools/call's in its two orphaned-response tests).
publish-outcome gains a sentence-shaped not-sent message. The fragment
names the write rejection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The host doesn't answer MCP ping from an MCPL server

1 participant