Skip to content

Commit 9e1b5da

Browse files
travisbreaksBr1an67
authored andcommitted
feat(core): add opt-in periodic ping for connection health monitoring
Implements the missing periodic ping functionality specified in issue #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 #1000 Co-authored-by: Br1an67 <932039080@qq.com>
1 parent 40174d2 commit 9e1b5da

File tree

5 files changed

+351
-1
lines changed

5 files changed

+351
-1
lines changed

.changeset/periodic-ping.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@modelcontextprotocol/core": minor
3+
"@modelcontextprotocol/client": minor
4+
"@modelcontextprotocol/server": minor
5+
---
6+
7+
feat: add opt-in periodic ping for connection health monitoring
8+
9+
Adds a `pingIntervalMs` option to `ProtocolOptions` that enables automatic
10+
periodic pings to verify the remote side is still responsive. Per the MCP
11+
specification, implementations SHOULD periodically issue pings to detect
12+
connection health, with configurable frequency.
13+
14+
The feature is disabled by default. When enabled, pings begin after
15+
initialization completes and stop automatically when the connection closes.
16+
Failures are reported via the `onerror` callback without stopping the timer.

packages/client/src/client/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,9 @@ export class Client extends Protocol<ClientContext> {
541541
this._setupListChangedHandlers(this._pendingListChangedConfig);
542542
this._pendingListChangedConfig = undefined;
543543
}
544+
545+
// Start periodic ping after successful initialization
546+
this.startPeriodicPing();
544547
} catch (error) {
545548
// Disconnect if initialization fails.
546549
void this.close();

packages/core/src/shared/protocol.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
TaskCreationParams
3535
} from '../types/index.js';
3636
import {
37+
EmptyResultSchema,
3738
getNotificationSchema,
3839
getRequestSchema,
3940
getResultSchema,
@@ -93,6 +94,21 @@ export type ProtocolOptions = {
9394
* so they should NOT be included here.
9495
*/
9596
tasks?: TaskManagerOptions;
97+
98+
/**
99+
* Interval (in milliseconds) between periodic ping requests sent to the remote side
100+
* to verify connection health. If set, pings will begin after {@linkcode Protocol.connect | connect()}
101+
* completes and stop automatically when the connection closes.
102+
*
103+
* Per the MCP specification, implementations SHOULD periodically issue pings to
104+
* detect connection health, with configurable frequency.
105+
*
106+
* Disabled by default (no periodic pings). Typical values: 15000-60000 (15s-60s).
107+
*
108+
* Ping failures are reported via the {@linkcode Protocol.onerror | onerror} callback
109+
* and do not stop the periodic timer.
110+
*/
111+
pingIntervalMs?: number;
96112
};
97113

98114
/**
@@ -309,6 +325,9 @@ export abstract class Protocol<ContextT extends BaseContext> {
309325

310326
private _taskManager: TaskManager;
311327

328+
private _pingTimer?: ReturnType<typeof setInterval>;
329+
private _pingIntervalMs?: number;
330+
312331
protected _supportedProtocolVersions: string[];
313332

314333
/**
@@ -337,6 +356,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
337356

338357
constructor(private _options?: ProtocolOptions) {
339358
this._supportedProtocolVersions = _options?.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
359+
this._pingIntervalMs = _options?.pingIntervalMs;
340360

341361
// Create TaskManager from protocol options
342362
this._taskManager = _options?.tasks ? new TaskManager(_options.tasks) : new NullTaskManager();
@@ -488,6 +508,8 @@ export abstract class Protocol<ContextT extends BaseContext> {
488508
}
489509

490510
private _onclose(): void {
511+
this.stopPeriodicPing();
512+
491513
const responseHandlers = this._responseHandlers;
492514
this._responseHandlers = new Map();
493515
this._progressHandlers.clear();
@@ -709,10 +731,56 @@ export abstract class Protocol<ContextT extends BaseContext> {
709731
return this._transport;
710732
}
711733

734+
/**
735+
* Starts sending periodic ping requests at the configured interval.
736+
* Pings are used to verify that the remote side is still responsive.
737+
* Failures are reported via the {@linkcode onerror} callback but do not
738+
* stop the timer; pings continue until the connection is closed.
739+
*
740+
* This is called automatically at the end of {@linkcode connect} when
741+
* `pingIntervalMs` is set. Subclasses that override `connect()` and
742+
* perform additional initialization (e.g., the MCP handshake) may call
743+
* this method after their initialization is complete instead.
744+
*
745+
* Has no effect if periodic ping is already running or if no interval
746+
* is configured.
747+
*/
748+
protected startPeriodicPing(): void {
749+
if (this._pingTimer || !this._pingIntervalMs) {
750+
return;
751+
}
752+
753+
this._pingTimer = setInterval(async () => {
754+
try {
755+
await this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, {
756+
timeout: this._pingIntervalMs
757+
});
758+
} catch (error) {
759+
this._onerror(error instanceof Error ? error : new Error(`Periodic ping failed: ${String(error)}`));
760+
}
761+
}, this._pingIntervalMs);
762+
763+
// Allow the process to exit even if the timer is still running
764+
if (typeof this._pingTimer === 'object' && 'unref' in this._pingTimer) {
765+
this._pingTimer.unref();
766+
}
767+
}
768+
769+
/**
770+
* Stops periodic ping requests. Called automatically when the connection closes.
771+
*/
772+
protected stopPeriodicPing(): void {
773+
if (this._pingTimer) {
774+
clearInterval(this._pingTimer);
775+
this._pingTimer = undefined;
776+
}
777+
}
778+
712779
/**
713780
* Closes the connection.
714781
*/
715782
async close(): Promise<void> {
783+
this.stopPeriodicPing();
716784
await this._transport?.close();
717785
}
718786

0 commit comments

Comments
 (0)