@@ -96,16 +96,17 @@ export type ProtocolOptions = {
9696
9797 /**
9898 * Interval (in milliseconds) between periodic ping requests sent to the remote side
99- * to verify connection health. If set, pings will begin after {@linkcode Protocol.connect | connect()}
100- * completes and stop automatically when the connection closes.
99+ * to verify connection health. When set, pings begin after initialization completes
100+ * ({@linkcode Client} starts them after the MCP handshake; {@linkcode Server} starts
101+ * them on `notifications/initialized`) and stop automatically when the connection closes.
101102 *
102103 * Per the MCP specification, implementations SHOULD periodically issue pings to
103104 * detect connection health, with configurable frequency.
104105 *
105106 * Disabled by default (no periodic pings). Typical values: 15000-60000 (15s-60s).
106107 *
107108 * Ping failures are reported via the {@linkcode Protocol.onerror | onerror} callback
108- * and do not stop the periodic timer .
109+ * and do not stop the periodic loop .
109110 */
110111 pingIntervalMs ?: number ;
111112} ;
@@ -324,8 +325,9 @@ export abstract class Protocol<ContextT extends BaseContext> {
324325
325326 private _taskManager : TaskManager ;
326327
327- private _pingTimer ?: ReturnType < typeof setInterval > ;
328+ private _pingTimer ?: ReturnType < typeof setTimeout > ;
328329 private _pingIntervalMs ?: number ;
330+ private _closing = false ;
329331
330332 protected _supportedProtocolVersions : string [ ] ;
331333
@@ -474,6 +476,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
474476 */
475477 async connect ( transport : Transport ) : Promise < void > {
476478 this . _transport = transport ;
479+ this . _closing = false ;
477480 const _onclose = this . transport ?. onclose ;
478481 this . _transport . onclose = ( ) => {
479482 try {
@@ -754,12 +757,13 @@ export abstract class Protocol<ContextT extends BaseContext> {
754757 * Starts sending periodic ping requests at the configured interval.
755758 * Pings are used to verify that the remote side is still responsive.
756759 * Failures are reported via the {@linkcode onerror} callback but do not
757- * stop the timer ; pings continue until the connection is closed.
760+ * stop the loop ; pings continue until the connection is closed.
758761 *
759- * This is called automatically at the end of {@linkcode connect} when
760- * `pingIntervalMs` is set. Subclasses that override `connect()` and
761- * perform additional initialization (e.g., the MCP handshake) may call
762- * this method after their initialization is complete instead.
762+ * This is not called automatically by the base {@linkcode Protocol.connect | connect()}
763+ * method. {@linkcode Client} calls it after the MCP initialization handshake
764+ * (and on reconnection), and {@linkcode Server} calls it when the
765+ * `notifications/initialized` notification is received. Custom `Protocol`
766+ * subclasses must call this explicitly after their own initialization.
763767 *
764768 * Has no effect if periodic ping is already running or if no interval
765769 * is configured.
@@ -769,28 +773,40 @@ export abstract class Protocol<ContextT extends BaseContext> {
769773 return ;
770774 }
771775
772- this . _pingTimer = setInterval ( async ( ) => {
773- try {
774- await this . _requestWithSchema ( { method : 'ping' } , EmptyResultSchema , {
775- timeout : this . _pingIntervalMs
776- } ) ;
777- } catch ( error ) {
778- this . _onerror ( error instanceof Error ? error : new Error ( `Periodic ping failed: ${ String ( error ) } ` ) ) ;
776+ const schedulePing = ( ) : void => {
777+ this . _pingTimer = setTimeout ( async ( ) => {
778+ try {
779+ await this . _requestWithSchema ( { method : 'ping' } , EmptyResultSchema , {
780+ timeout : this . _pingIntervalMs
781+ } ) ;
782+ } catch ( error ) {
783+ // Suppress errors caused by intentional shutdown
784+ if ( ! this . _closing ) {
785+ this . _onerror ( error instanceof Error ? error : new Error ( `Periodic ping failed: ${ String ( error ) } ` ) ) ;
786+ }
787+ } finally {
788+ // Schedule the next ping only if we have not been stopped
789+ if ( this . _pingTimer ) {
790+ schedulePing ( ) ;
791+ }
792+ }
793+ } , this . _pingIntervalMs ) ;
794+
795+ // Allow the process to exit even if the timer is still running
796+ if ( typeof this . _pingTimer === 'object' && 'unref' in this . _pingTimer ) {
797+ this . _pingTimer . unref ( ) ;
779798 }
780- } , this . _pingIntervalMs ) ;
799+ } ;
781800
782- // Allow the process to exit even if the timer is still running
783- if ( typeof this . _pingTimer === 'object' && 'unref' in this . _pingTimer ) {
784- this . _pingTimer . unref ( ) ;
785- }
801+ schedulePing ( ) ;
786802 }
787803
788804 /**
789805 * Stops periodic ping requests. Called automatically when the connection closes.
790806 */
791807 protected stopPeriodicPing ( ) : void {
792808 if ( this . _pingTimer ) {
793- clearInterval ( this . _pingTimer ) ;
809+ clearTimeout ( this . _pingTimer ) ;
794810 this . _pingTimer = undefined ;
795811 }
796812 }
@@ -799,6 +815,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
799815 * Closes the connection.
800816 */
801817 async close ( ) : Promise < void > {
818+ this . _closing = true ;
802819 this . stopPeriodicPing ( ) ;
803820 await this . _transport ?. close ( ) ;
804821 }
0 commit comments