feat(skill-mcp): add auto-reconnect retry on "Not connected" errors

- Added withOperationRetry<T>() helper method that retries operations up to 3 times
- Catches "Not connected" errors (case-insensitive)
- Cleans up stale client before retry
- Modified callTool, readResource, getPrompt to use retry logic
- Added tests for retry behavior (3 new test cases)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
justsisyphus
2026-01-17 18:14:48 +09:00
parent dec35d28a7
commit c433e7397e
2 changed files with 161 additions and 9 deletions
+55 -9
View File
@@ -415,9 +415,10 @@ export class SkillMcpManager {
name: string,
args: Record<string, unknown>
): Promise<unknown> {
const client = await this.getOrCreateClientWithRetry(info, context.config)
const result = await client.callTool({ name, arguments: args })
return result.content
return this.withOperationRetry(info, context.config, async (client) => {
const result = await client.callTool({ name, arguments: args })
return result.content
})
}
async readResource(
@@ -425,9 +426,10 @@ export class SkillMcpManager {
context: SkillMcpServerContext,
uri: string
): Promise<unknown> {
const client = await this.getOrCreateClientWithRetry(info, context.config)
const result = await client.readResource({ uri })
return result.contents
return this.withOperationRetry(info, context.config, async (client) => {
const result = await client.readResource({ uri })
return result.contents
})
}
async getPrompt(
@@ -436,9 +438,53 @@ export class SkillMcpManager {
name: string,
args: Record<string, string>
): Promise<unknown> {
const client = await this.getOrCreateClientWithRetry(info, context.config)
const result = await client.getPrompt({ name, arguments: args })
return result.messages
return this.withOperationRetry(info, context.config, async (client) => {
const result = await client.getPrompt({ name, arguments: args })
return result.messages
})
}
private async withOperationRetry<T>(
info: SkillMcpClientInfo,
config: ClaudeCodeMcpServer,
operation: (client: Client) => Promise<T>
): Promise<T> {
const maxRetries = 3
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const client = await this.getOrCreateClientWithRetry(info, config)
return await operation(client)
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
const errorMessage = lastError.message.toLowerCase()
if (!errorMessage.includes("not connected")) {
throw lastError
}
if (attempt === maxRetries) {
throw new Error(
`Failed after ${maxRetries} reconnection attempts: ${lastError.message}`
)
}
const key = this.getClientKey(info)
const existing = this.clients.get(key)
if (existing) {
this.clients.delete(key)
try {
await existing.client.close()
} catch { /* process may already be terminated */ }
try {
await existing.transport.close()
} catch { /* transport may already be terminated */ }
}
}
}
throw lastError || new Error("Operation failed with unknown error")
}
private async getOrCreateClientWithRetry(