@civic/hub-bridge
Version:
Stdio <-> HTTP/SSE MCP bridge with Civic auth handling
107 lines • 5.16 kB
JavaScript
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