Skip to content

Commit 51a9eae

Browse files
travisbreaksclaude
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: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ccb78f2 commit 51a9eae

File tree

5 files changed

+350
-1
lines changed

5 files changed

+350
-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
@@ -514,6 +514,9 @@ export class Client extends Protocol<ClientContext> {
514514
this._setupListChangedHandlers(this._pendingListChangedConfig);
515515
this._pendingListChangedConfig = undefined;
516516
}
517+
518+
// Start periodic ping after successful initialization
519+
this.startPeriodicPing();
517520
} catch (error) {
518521
// Disconnect if initialization fails.
519522
void this.close();

packages/core/src/shared/protocol.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type {
4343
import {
4444
CancelTaskResultSchema,
4545
CreateTaskResultSchema,
46+
EmptyResultSchema,
4647
getNotificationSchema,
4748
getRequestSchema,
4849
getResultSchema,
@@ -119,6 +120,20 @@ export type ProtocolOptions = {
119120
* appropriately (e.g., by failing the task, dropping messages, etc.).
120121
*/
121122
maxTaskQueueSize?: number;
123+
/**
124+
* Interval (in milliseconds) between periodic ping requests sent to the remote side
125+
* to verify connection health. If set, pings will begin after {@linkcode Protocol.connect | connect()}
126+
* completes and stop automatically when the connection closes.
127+
*
128+
* Per the MCP specification, implementations SHOULD periodically issue pings to
129+
* detect connection health, with configurable frequency.
130+
*
131+
* Disabled by default (no periodic pings). Typical values: 15000-60000 (15s-60s).
132+
*
133+
* Ping failures are reported via the {@linkcode Protocol.onerror | onerror} callback
134+
* and do not stop the periodic timer.
135+
*/
136+
pingIntervalMs?: number;
122137
};
123138

124139
/**
@@ -413,6 +428,9 @@ export abstract class Protocol<ContextT extends BaseContext> {
413428

414429
private _requestResolvers: Map<RequestId, (response: JSONRPCResultResponse | Error) => void> = new Map();
415430

431+
private _pingTimer?: ReturnType<typeof setInterval>;
432+
private _pingIntervalMs?: number;
433+
416434
protected _supportedProtocolVersions: string[];
417435

418436
/**
@@ -441,6 +459,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
441459

442460
constructor(private _options?: ProtocolOptions) {
443461
this._supportedProtocolVersions = _options?.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
462+
this._pingIntervalMs = _options?.pingIntervalMs;
444463

445464
this.setNotificationHandler('notifications/cancelled', notification => {
446465
this._oncancel(notification);
@@ -724,6 +743,8 @@ export abstract class Protocol<ContextT extends BaseContext> {
724743
}
725744

726745
private _onclose(): void {
746+
this.stopPeriodicPing();
747+
727748
const responseHandlers = this._responseHandlers;
728749
this._responseHandlers = new Map();
729750
this._progressHandlers.clear();
@@ -992,10 +1013,56 @@ export abstract class Protocol<ContextT extends BaseContext> {
9921013
return this._transport;
9931014
}
9941015

1016+
/**
1017+
* Starts sending periodic ping requests at the configured interval.
1018+
* Pings are used to verify that the remote side is still responsive.
1019+
* Failures are reported via the {@linkcode onerror} callback but do not
1020+
* stop the timer; pings continue until the connection is closed.
1021+
*
1022+
* This is called automatically at the end of {@linkcode connect} when
1023+
* `pingIntervalMs` is set. Subclasses that override `connect()` and
1024+
* perform additional initialization (e.g., the MCP handshake) may call
1025+
* this method after their initialization is complete instead.
1026+
*
1027+
* Has no effect if periodic ping is already running or if no interval
1028+
* is configured.
1029+
*/
1030+
protected startPeriodicPing(): void {
1031+
if (this._pingTimer || !this._pingIntervalMs) {
1032+
return;
1033+
}
1034+
1035+
this._pingTimer = setInterval(async () => {
1036+
try {
1037+
await this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, {
1038+
timeout: this._pingIntervalMs
1039+
});
1040+
} catch (error) {
1041+
this._onerror(error instanceof Error ? error : new Error(`Periodic ping failed: ${String(error)}`));
1042+
}
1043+
}, this._pingIntervalMs);
1044+
1045+
// Allow the process to exit even if the timer is still running
1046+
if (typeof this._pingTimer === 'object' && 'unref' in this._pingTimer) {
1047+
this._pingTimer.unref();
1048+
}
1049+
}
1050+
1051+
/**
1052+
* Stops periodic ping requests. Called automatically when the connection closes.
1053+
*/
1054+
protected stopPeriodicPing(): void {
1055+
if (this._pingTimer) {
1056+
clearInterval(this._pingTimer);
1057+
this._pingTimer = undefined;
1058+
}
1059+
}
1060+
9951061
/**
9961062
* Closes the connection.
9971063
*/
9981064
async close(): Promise<void> {
1065+
this.stopPeriodicPing();
9991066
await this._transport?.close();
10001067
}
10011068

0 commit comments

Comments
 (0)