@@ -18,6 +18,10 @@ export class SSHConnectionManager {
1818 private reconnectAttempts : number = 0 ;
1919 private maxReconnectAttempts : number = 5 ;
2020 private baseBackoffMs : number = 1000 ;
21+ private commandTimeoutMs : number ;
22+ private maxConsecutiveFailures : number ;
23+ private consecutiveFailures : number = 0 ;
24+ private circuitBreakerOpen : boolean = false ;
2125
2226 constructor ( ) {
2327 this . ssh = new NodeSSH ( ) ;
@@ -46,6 +50,14 @@ export class SSHConnectionManager {
4650 privateKeyPath,
4751 password,
4852 } ;
53+
54+ // Load timeout and circuit breaker configuration
55+ this . commandTimeoutMs = process . env . COMMAND_TIMEOUT_MS
56+ ? parseInt ( process . env . COMMAND_TIMEOUT_MS )
57+ : 15000 ; // Default: 15 seconds
58+ this . maxConsecutiveFailures = process . env . MAX_CONSECUTIVE_FAILURES
59+ ? parseInt ( process . env . MAX_CONSECUTIVE_FAILURES )
60+ : 3 ; // Default: 3 consecutive failures
4961 }
5062
5163 /**
@@ -93,36 +105,89 @@ export class SSHConnectionManager {
93105 }
94106
95107 /**
96- * Execute command via SSH
108+ * Execute command via SSH with timeout and circuit breaker protection
97109 */
98110 async executeCommand ( command : string ) : Promise < { stdout : string ; stderr : string ; exitCode : number } > {
111+ // Check circuit breaker
112+ if ( this . circuitBreakerOpen ) {
113+ throw new Error (
114+ `Circuit breaker is open after ${ this . consecutiveFailures } consecutive failures. ` +
115+ `Please check server health or restart the MCP server to reset.`
116+ ) ;
117+ }
118+
119+ // Timeout ID for cleanup
120+ let timeoutId : NodeJS . Timeout | null = null ;
121+
99122 try {
100123 if ( ! this . connected ) {
101124 await this . connect ( ) ;
102125 }
103126
104- const result = await this . ssh . execCommand ( command ) ;
127+ // Create a timeout promise
128+ const timeoutPromise = new Promise < never > ( ( _ , reject ) => {
129+ timeoutId = setTimeout ( ( ) => {
130+ reject ( new Error ( `TIMEOUT: Command timed out after ${ this . commandTimeoutMs } ms` ) ) ;
131+ } , this . commandTimeoutMs ) ;
132+ } ) ;
133+
134+ // Race between command execution and timeout
135+ const result = await Promise . race ( [
136+ this . ssh . execCommand ( command ) ,
137+ timeoutPromise ,
138+ ] ) ;
139+
140+ // Clear timeout on success
141+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
142+
143+ // Reset circuit breaker on successful command
144+ this . consecutiveFailures = 0 ;
145+ this . circuitBreakerOpen = false ;
105146
106147 return {
107148 stdout : result . stdout ,
108149 stderr : result . stderr ,
109150 exitCode : result . code ?? 0 ,
110151 } ;
111152 } catch ( error ) {
112- // Attempt to reconnect on connection errors
113- if ( error instanceof Error && error . message . includes ( "connection" ) ) {
153+ // Clear timeout on error
154+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
155+
156+ // Increment failure counter
157+ this . consecutiveFailures ++ ;
158+
159+ // Open circuit breaker if threshold reached
160+ if ( this . consecutiveFailures >= this . maxConsecutiveFailures ) {
161+ this . circuitBreakerOpen = true ;
162+ console . error (
163+ `Circuit breaker opened after ${ this . consecutiveFailures } consecutive failures. ` +
164+ `Future commands will fail immediately until the MCP server is restarted.`
165+ ) ;
166+ }
167+
168+ // Determine error type for better error messages
169+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
170+ const isTimeout = errorMessage . includes ( "TIMEOUT:" ) ;
171+ const isConnection = errorMessage . toLowerCase ( ) . includes ( "connection" ) ;
172+
173+ if ( isTimeout ) {
174+ throw new Error (
175+ `Command timed out after ${ this . commandTimeoutMs } ms. ` +
176+ `The command may be hung or taking too long. ` +
177+ `Consider increasing COMMAND_TIMEOUT_MS if this is a long-running operation.`
178+ ) ;
179+ }
180+
181+ if ( isConnection ) {
114182 this . connected = false ;
115- await this . reconnect ( ) ;
116- // Retry the command after reconnection
117- const result = await this . ssh . execCommand ( command ) ;
118- return {
119- stdout : result . stdout ,
120- stderr : result . stderr ,
121- exitCode : result . code ?? 0 ,
122- } ;
183+ throw new Error (
184+ `SSH connection lost: ${ errorMessage } . ` +
185+ `The MCP server will attempt to reconnect on the next command. ` +
186+ `If this persists, check your network connection and SSH credentials.`
187+ ) ;
123188 }
124189
125- throw new Error ( `Failed to execute command: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
190+ throw new Error ( `Failed to execute command: ${ errorMessage } ` ) ;
126191 }
127192 }
128193
0 commit comments