Skip to content

feat(core): add opt-in periodic ping for connection health monitoring#1717

Open
travisbreaks wants to merge 2 commits intomodelcontextprotocol:mainfrom
travisbreaks:feat/periodic-ping
Open

feat(core): add opt-in periodic ping for connection health monitoring#1717
travisbreaks wants to merge 2 commits intomodelcontextprotocol:mainfrom
travisbreaks:feat/periodic-ping

Conversation

@travisbreaks
Copy link
Copy Markdown

Summary

  • Adds pingIntervalMs option to ProtocolOptions that enables automatic periodic pings to verify the remote side is still responsive
  • Implemented at the Protocol level so both Client and Server benefit
  • Client starts periodic pings after successful initialization; Server starts after receiving the initialized notification
  • Disabled by default (opt-in). Ping failures are reported via onerror without stopping the timer
  • Timer uses unref() so it does not prevent clean Node.js process exit

Per the MCP specification: "Implementations SHOULD periodically issue pings to detect connection health" with "configurable frequency."

Usage

const client = new Client(
    { name: 'my-client', version: '1.0.0' },
    { pingIntervalMs: 30_000 } // ping every 30 seconds
);

client.onerror = (error) => {
    console.warn('Connection issue:', error.message);
};

await client.connect(transport);
// Periodic pings start automatically after initialization

Key design decisions

  • Protocol-level implementation: both Client and Server can use periodic pings, since the spec says "implementations" (not just clients)
  • Opt-in, not opt-out: existing behavior is unchanged; you must explicitly set pingIntervalMs to enable
  • Ping timeout equals the interval: if a ping takes longer than one interval to respond, it fails with a timeout error rather than stacking up
  • unref() on the timer: the periodic ping does not keep the Node.js process alive if all other work is done

Relationship to other PRs

This complements PR #1712 (host process watchdog for stdio orphan detection). That PR detects when the host process is gone at the OS level; this PR detects when the remote MCP peer is unresponsive at the protocol level.

Fixes #1000

Test plan

  • should not send periodic pings when pingIntervalMs is not set (default behavior unchanged)
  • should send periodic pings when pingIntervalMs is set and startPeriodicPing is called
  • should stop periodic pings on close
  • should report ping errors via onerror without stopping the timer
  • should not start duplicate timers if startPeriodicPing is called multiple times
  • should stop periodic pings when transport closes unexpectedly
  • All 446 core tests pass
  • All 386 integration tests pass
  • Build succeeds
  • Lint passes

Changeset

@modelcontextprotocol/core: minor, @modelcontextprotocol/client: minor, @modelcontextprotocol/server: minor

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

@travisbreaks travisbreaks requested a review from a team as a code owner March 19, 2026 18:44
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 810c72e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/node Major
@modelcontextprotocol/express Major
@modelcontextprotocol/hono Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 19, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1717

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1717

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1717

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1717

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1717

commit: 810c72e

@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Implements the missing periodic ping functionality specified in issue modelcontextprotocol#1000.
Per the MCP specification, implementations SHOULD periodically issue pings
to detect connection health, with configurable frequency.

Changes:
- Add `pingIntervalMs` option to `ProtocolOptions` (disabled by default)
- Implement `startPeriodicPing()` and `stopPeriodicPing()` in Protocol
- Client starts periodic ping after successful initialization
- Server starts periodic ping after receiving initialized notification
- Timer uses `unref()` so it does not prevent clean process exit
- Ping failures are reported via `onerror` without stopping the timer
- Timer is automatically cleaned up on close or unexpected disconnect

Fixes modelcontextprotocol#1000

Co-authored-by: Br1an67 <932039080@qq.com>
Mirrors the Go SDK pattern where the initialized notification handler
is a named method rather than an inline closure.
@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Comment on lines +544 to +546

// Start periodic ping after successful initialization
this.startPeriodicPing();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Periodic pings are not restarted after client reconnection. The reconnection path (line 497-501) returns early when transport.sessionId is set, skipping the startPeriodicPing() call at line 546. Since _onclose() clears the ping timer during disconnection, connection health monitoring silently stops working after any reconnect cycle. Fix: add this.startPeriodicPing() before the return at line 501.

Extended reasoning...

What the bug is

In Client.connect(), there are two code paths: the initial connection path (which performs the full MCP initialization handshake) and the reconnection path (when transport.sessionId is already set). The startPeriodicPing() call at line 546 is only reached via the initial connection path. The reconnection path at lines 497-501 returns early, skipping it entirely.

How it manifests

When a client disconnects and reconnects (e.g., due to a transient network failure with an HTTP transport), the following sequence occurs:

  1. The old transport closes, triggering _onclose() in Protocol, which calls stopPeriodicPing() — this clears _pingTimer via clearInterval.
  2. The application calls client.connect(newTransport) where newTransport.sessionId is already set (since it is a reconnection to an existing session).
  3. super.connect(transport) is called, setting up the new transport.
  4. The guard transport.sessionId \!== undefined evaluates to true, so the method sets the protocol version and returns at line 501.
  5. startPeriodicPing() at line 546 is never reached.
  6. Since _pingTimer was cleared in step 1 and _pingIntervalMs is still set from construction, startPeriodicPing() would successfully start a new timer — but it is simply never called.

Why existing code does not prevent it

The startPeriodicPing() method has a guard if (this._pingTimer || \!this._pingIntervalMs) that prevents duplicate timers, but after _onclose() runs, _pingTimer is undefined, so the guard would allow the timer to be started. The problem is purely that the method is never invoked in the reconnection path. Neither super.connect() (i.e., Protocol.connect()) nor any other part of the reconnection flow calls it.

Impact

Any client that uses pingIntervalMs for connection health monitoring will silently lose that monitoring after the first reconnection. This is particularly problematic because reconnections are exactly the scenario where health monitoring is most valuable — the connection was already unstable once, and the user wants to know if it becomes unresponsive again. The client will believe pings are active (since _pingIntervalMs is still set), but no pings will be sent.

Fix

Add this.startPeriodicPing() to the reconnection branch in Client.connect(), just before the return statement at line 501:

if (transport.sessionId \!== undefined) {
    if (this._negotiatedProtocolVersion \!== undefined && transport.setProtocolVersion) {
        transport.setProtocolVersion(this._negotiatedProtocolVersion);
    }
    this.startPeriodicPing();
    return;
}

The idempotency guard in startPeriodicPing() ensures this is safe even if called multiple times.

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.

Bug Report-2: Missing Periodic Ping Implementation

2 participants