UNPKG

@civic/hub-bridge

Version:

Stdio <-> HTTP/SSE MCP bridge with Civic auth handling

107 lines 5.16 kB
import { AbstractHook } from "@civic/hook-common"; import { logger } from "../utils/logger.js"; import { getPassthroughServerContext, isPassthroughServerContext } from "@civic/passthrough-mcp-server"; /** * SessionRecoveryHook detects when the server has dropped the session * and automatically recreates the client connection. * * This hook monitors responses for the specific error message: * "Bad Request: No valid session ID provided or not an initialize request" * which indicates the server has lost the session. */ export class SessionRecoveryHook extends AbstractHook { isRecovering = false; get name() { return "SessionRecoveryHook"; } /** * Check if the error indicates a dropped session */ isSessionDroppedError(error) { // Check for Error object with session drop message if (error instanceof Error) { const isSessionError = error.message.includes("No valid session ID provided or not an initialize request"); if (isSessionError) { logger.info(`SessionRecoveryHook: Detected session drop: ${error.message}`); return true; } } return false; } async processToolException(toolError, originalToolCall, context) { try { // Skip if we're already in recovery process to avoid recursion if (this.isRecovering) { return { response: "continue" }; } if (!context || !isPassthroughServerContext(context)) { logger.error(`Hook ${this.name} is not running in PassthroughServerContext, skipping session recovery. Context: ${JSON.stringify(context)}`); return { response: "continue" }; } // Check if this error indicates a session drop const isSessionDropped = this.isSessionDroppedError(toolError); if (isSessionDropped) { logger.info(`SessionRecoveryHook: Starting session recovery for tool: ${originalToolCall.name}`); // Set recovery flag to prevent recursion this.isRecovering = true; try { const passthroughServerHookContext = getPassthroughServerContext(context); // Recreate the client const newClient = await passthroughServerHookContext.recreateClient(); // Now retry the original tool call with the new client try { // Call the target client's tool with arguments (potentially modified by hook) const toolCallWithArgs = { ...originalToolCall, arguments: typeof originalToolCall.arguments === "object" && originalToolCall.arguments !== null ? originalToolCall.arguments : undefined, }; const result = await newClient.callTool(toolCallWithArgs); logger.info(`SessionRecoveryHook: Tool call succeeded after recovery`); return { response: "abort", body: result, reason: "Session was invalid an needed to be recovered", }; } catch (retryError) { logger.error(`SessionRecoveryHook: Tool call failed after recovery:`, retryError); // Return the retry error instead of the original session error return { response: "abort", body: { jsonrpc: "2.0", error: { code: -32603, message: "Tool call failed after session recovery.", data: { recoveryPerformed: true, originalError: toolError, retryError: retryError } }, id: null } }; } } finally { // Always reset the recovery flag this.isRecovering = false; } } // Continue with the original error if no session drop detected return { response: "continue" }; } catch (processingError) { logger.error("SessionRecoveryHook: Error in processToolException:", processingError); // Reset recovery flag on error this.isRecovering = false; // Continue with original error on processing failure to avoid breaking the flow return { response: "continue" }; } } } //# sourceMappingURL=session-recovery-hook.js.map