@@ -4,7 +4,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
4
4
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
5
5
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
6
6
import { OAuthClientInformationFull , OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js'
7
- import { OAuthCallbackServerOptions } from './types'
7
+ import { OAuthCallbackServerOptions , PingConfig } from './types'
8
8
import { getConfigFilePath , readJsonFile } from './mcp-auth-config'
9
9
import express from 'express'
10
10
import net from 'net'
@@ -14,6 +14,7 @@ import fs from 'fs/promises'
14
14
// Connection constants
15
15
export const REASON_AUTH_NEEDED = 'authentication-needed'
16
16
export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport'
17
+ export const PING_INTERVAL_DEFAULT = 30000
17
18
18
19
// Transport strategy types
19
20
export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
@@ -91,6 +92,44 @@ export type AuthInitializer = () => Promise<{
91
92
skipBrowserAuth : boolean
92
93
} >
93
94
95
+ /**
96
+ * Sets up periodic ping to keep the connection alive
97
+ * @param transport The transport to ping
98
+ * @param config Ping configuration
99
+ * @returns A cleanup function to stop pinging
100
+ */
101
+ export function setupPing ( transport : Transport , config : PingConfig ) : ( ) => void {
102
+ if ( ! config . enabled ) {
103
+ return ( ) => { }
104
+ }
105
+
106
+ let pingTimeout : NodeJS . Timeout | null = null
107
+ let lastPingId = 0
108
+
109
+ const interval = config . interval * 1000 // convert ms to s
110
+ const pingInterval = setInterval ( async ( ) => {
111
+ const pingId = ++ lastPingId
112
+ try {
113
+ // Docs: https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/ping
114
+ await transport . send ( {
115
+ jsonrpc : '2.0' ,
116
+ id : `ping-${ pingId } ` ,
117
+ method : 'ping' ,
118
+ } )
119
+ log ( `Ping ${ pingId } successful` )
120
+ } catch ( error ) {
121
+ log ( `Ping ${ pingId } failed:` , error )
122
+ }
123
+ } , interval )
124
+
125
+ return ( ) => {
126
+ if ( pingTimeout ) {
127
+ clearTimeout ( pingTimeout )
128
+ }
129
+ clearInterval ( pingInterval )
130
+ }
131
+ }
132
+
94
133
/**
95
134
* Creates and connects to a remote server with OAuth authentication
96
135
* @param client The client to connect with
@@ -432,6 +471,21 @@ export async function parseCommandLineArgs(args: string[], usage: string) {
432
471
i ++
433
472
}
434
473
474
+ // Parse ping configuration
475
+ const keepAlive = args . includes ( '--keep-alive' )
476
+ const pingIntervalIndex = args . indexOf ( '--ping-interval' )
477
+ let pingInterval = PING_INTERVAL_DEFAULT
478
+ if ( pingIntervalIndex !== - 1 && pingIntervalIndex < args . length - 1 ) {
479
+ const intervalStr = args [ pingIntervalIndex + 1 ]
480
+ const interval = parseInt ( intervalStr )
481
+ if ( ! isNaN ( interval ) && interval > 0 ) {
482
+ pingInterval = interval
483
+ log ( `Using ping interval: ${ pingInterval } seconds` )
484
+ } else {
485
+ log ( `Warning: Invalid ping interval "${ args [ pingIntervalIndex + 1 ] } ". Using default: ${ PING_INTERVAL_DEFAULT } seconds` )
486
+ }
487
+ }
488
+
435
489
const serverUrl = args [ 0 ]
436
490
const specifiedPort = args [ 1 ] ? parseInt ( args [ 1 ] ) : undefined
437
491
const allowHttp = args . includes ( '--allow-http' )
@@ -505,7 +559,16 @@ export async function parseCommandLineArgs(args: string[], usage: string) {
505
559
} )
506
560
}
507
561
508
- return { serverUrl, callbackPort, headers, transportStrategy }
562
+ return {
563
+ serverUrl,
564
+ callbackPort,
565
+ headers,
566
+ transportStrategy,
567
+ pingConfig : {
568
+ enabled : keepAlive ,
569
+ interval : pingInterval ,
570
+ } ,
571
+ }
509
572
}
510
573
511
574
/**
0 commit comments