@civic/hub-bridge
Version:
Stdio <-> HTTP/SSE MCP bridge with Civic auth handling
82 lines • 3.78 kB
JavaScript
import { AbstractHook } from "@civic/hook-common";
import { ServiceAuthorizationHandler } from "../auth/service-authorization.js";
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
import { logger } from "../utils/logger.js";
import { getPassthroughServerContext, isPassthroughServerContext } from "@civic/passthrough-mcp-server";
/**
* AuthenticationHook listens for special authentication messages and triggers
* authentication flows when needed.
*
* This hook processes incoming tool calls and responses to detect when
* authentication is required and handles the authentication flow.
*/
export class AuthenticationHook extends AbstractHook {
authProvider;
constructor(authProvider) {
super();
this.authProvider = authProvider;
}
get name() {
return "AuthenticationHook";
}
/**
* Check if a response is an authorization required response
*/
isAuthorizationRequiredResponse(result) {
return (result.content?.length === 3 &&
result.content[0].type === 'text' &&
result.content[1].type === 'resource' &&
result.content[2].type === 'resource' &&
result.content[1].resource.name === 'authorization_url' &&
result.content[2].resource.name === 'continue_job_id');
}
/**
* Detect if a response contains service authorization flow information
*/
detectServiceAuthorizationFlow(result) {
logger.debug("Checking for service authorization flow in response");
if (!this.isAuthorizationRequiredResponse(result)) {
return null;
}
logger.info("Service authorization flow detected");
const authUrl = result.content[1].resource.text;
const continueJobId = result.content[2].resource.text;
logger.debug(`Authorization URL: ${authUrl}`);
logger.debug(`Continue job ID: ${continueJobId}`);
return { authUrl, continueJobId };
}
async processResponse(response, originalToolCall, context) {
try {
// Validate that the response is a valid ToolCallResult
const parseResult = CallToolResultSchema.safeParse(response);
if (!parseResult.success) {
logger.debug("Response is not a valid ToolCallResult, skipping auth processing");
return { response: "continue" };
}
const toolCallResult = parseResult.data;
// Check if this response requires auth
const authInfo = this.detectServiceAuthorizationFlow(toolCallResult);
// If no auth required, just return the original response
if (!authInfo) {
return { response: "continue", body: toolCallResult };
}
if (!context || !isPassthroughServerContext(context)) {
logger.error(`Hook ${this.name} is not running in PassthroughServerContext, skipping session recovery. Context: ${JSON.stringify(context)}`);
return { response: "continue" };
}
// Create auth handler and process the response
const authHandler = new ServiceAuthorizationHandler(getPassthroughServerContext(context).getTargetClient(), this.authProvider);
const processedResponse = await authHandler.handleServiceAuthorization(toolCallResult);
return {
response: "continue",
body: processedResponse
};
}
catch (error) {
logger.error("Error in AuthenticationHook.processResponse:", error);
// Continue with original response on error to avoid breaking the flow
return { response: "continue" };
}
}
}
//# sourceMappingURL=authentication-hook.js.map