UNPKG

@civic/hub-bridge

Version:

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

204 lines 8.8 kB
// Hub Bridge - MCP Bridge with Civic Auth // This is a rewritten version of the hub-bridge package import { createStdioPassthroughProxy } from "@civic/passthrough-mcp-server"; import { REMOTE_MCP_URL } from "./config/index.js"; import { AuthenticationHook } from "./hooks/index.js"; import { CLIAuthProviderSingleton } from "./auth/cli-auth-provider-singleton.js"; import * as configVars from './config/index.js'; import { handleCliArguments, getVersion } from './cli/index.js'; import { logger } from "./utils/index.js"; import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js"; import { HubAuthFailureInterceptor } from "./hooks/hub-auth-failure-interceptor.js"; import { LocalToolsHook } from "./hooks/local-tools-hook.js"; import { InitializeLogHook } from "./hooks/initialize-log-hook.js"; import { ConfigManager } from "./lib/ConfigManager.js"; import { createHubBridgeStreamableHttpTransport } from "./transport/index.js"; // Global state for graceful shutdown let proxy = null; let isShuttingDown = false; // Prevent double shutdown calls // Log configuration values const logConfig = async () => { const version = await getVersion(); logger.info(`Civic Hub Bridge v${version} (Node ${process.version})`); logger.info('Configuration:'); logger.info('MCP_REMOTE_URL:', configVars.REMOTE_MCP_URL); logger.info('CLIENT_ID:', configVars.CLIENT_ID); logger.info('NO_LOGIN:', configVars.NO_LOGIN); logger.info('NO_AUTH_CAPTURE:', configVars.NO_AUTH_CAPTURE); }; // Redirect console.log to stderr to avoid interfering with MCP JSON messages // Note: Our custom logger (utils/logger.ts) already writes directly to stderr, but // these redirects ensure that any direct console.log calls or third-party libraries // that use console methods also have their output redirected to stderr. This provides // a comprehensive solution where ALL output is consistently sent to stderr regardless // of its source, maintaining clean stdout for the MCP protocol. console.log = (...args) => { process.stderr.write(`LOG: ${args.join(' ')}\n`); }; console.error = (...args) => { process.stderr.write(`ERROR: ${args.join(' ')}\n`); }; console.warn = (...args) => { process.stderr.write(`WARN: ${args.join(' ')}\n`); }; console.info = (...args) => { process.stderr.write(`INFO: ${args.join(' ')}\n`); }; /** * Graceful shutdown handler */ async function shutdown(signal) { if (isShuttingDown) return; isShuttingDown = true; logger.info(`\nReceived ${signal}. Shutting down gracefully...`); if (proxy) { try { await proxy.stop(); logger.info("Proxy stopped successfully."); } catch (error) { logger.error("Error stopping proxy:", error); } } logger.info("Shutdown complete."); // eslint-disable-next-line no-process-exit process.exit(0); // Node will exit naturally after all event handlers complete } /** * Main function to start the passthrough MCP server */ async function main() { // Handle CLI arguments (version, help, install) const shouldExit = await handleCliArguments(); if (shouldExit) return; // Log configuration information await logConfig(); // Load configuration logger.info("Loading configuration..."); const configmanager = ConfigManager.getInstance(); const currentProfile = await configmanager.getCurrentProfileAlias(); logger.info(`Current profile: ${currentProfile}`); try { // Get the singleton auth provider const authProvider = CLIAuthProviderSingleton.getInstance(); const authenticationHook = new AuthenticationHook(authProvider); const hubAuthFailureInterceptor = new HubAuthFailureInterceptor(); const stdioProxy = await createStdioPassthroughProxy({ hooks: [ new InitializeLogHook(), new LocalToolsHook(), authenticationHook, hubAuthFailureInterceptor, // new SessionRecoveryHook() ], target: { transportType: 'custom', transportFactory: () => createHubBridgeStreamableHttpTransport(currentProfile) }, autoStart: false }); proxy = stdioProxy; const continueJobCallback = async (jobId) => { return stdioProxy.proxyContext.target.request({ method: "tools/call", params: { name: "continue_job", arguments: { jobId: jobId } } }, CallToolResultSchema); }; authenticationHook.setContinueJobCallback(continueJobCallback); hubAuthFailureInterceptor.setTransportInterface(stdioProxy.proxyContext.target); // proxy.server.addTool({ // name: localTool.name, // description: localTool.description, // parameters: z.object(zodProperties), // execute: async (_args) => { // // Import the handler dynamically to avoid circular imports // const { toolHandlers } = await import('./tools/index.js'); // const handler = toolHandlers[localTool.name]; // if (handler) { // // Create a proper MCP request object // const request = CallToolRequestSchema.parse({ // jsonrpc: "2.0", // id: 0, // method: "tools/call", // params: { // name: localTool.name, // arguments: _args || {} // } // }); // // // Call the handler with the properly formatted request // const result = await handler(request); // // // Convert from CallToolResult to FastMCP expected format // if (result.content && result.content.length > 0) { // // Return the first text content as a simple string // const firstContent = result.content[0]; // if (firstContent.type === 'text') { // return firstContent.text; // } // } // // return result.isError ? "Operation failed" : "Operation completed"; // } // throw new Error(`No handler found for local tool: ${localTool.name}`); // } // }); // } // Start manually later await proxy.start(); // trigger auth // remove if required. // await stdioProxy.proxyContext.target.ping(); // TODO: Integrate ClientConnectionMonitor for detecting client disconnection // The ClientConnectionMonitor class is available in utils/client-detection.ts and needs to be // integrated with the FastMCP server sessions. Need to: // 1. Find the correct way to access MCP Server instances from FastMCP sessions // 2. Monitor for new client connections and start ClientConnectionMonitor for each // 3. Handle cleanup when sessions disconnect // 4. Implement graceful shutdown when client disconnection is detected logger.info(`Passthrough Stdio MCP Server running with httpStream transport, connecting to target at ${REMOTE_MCP_URL}`); } catch (error) { logger.error("Error details:", error); logger.error("Failed to start server1: " + error); throw new Error(`Failed to start server: ${error}`); } } // Process signal handlers for graceful shutdown process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); // Basic error handling process.on('uncaughtException', (error, origin) => { logger.error(`\nFATAL UNCAUGHT EXCEPTION (${origin}):`, error); // Attempt graceful shutdown, but it might fail if state is corrupted shutdown('uncaughtException').catch(() => { logger.error('Failed to shutdown gracefully after uncaught exception'); throw error; }); }); process.on('unhandledRejection', (reason) => { logger.error('\nFATAL UNHANDLED REJECTION:'); logger.error('Reason:', reason); // Attempt graceful shutdown shutdown('unhandledRejection').catch(() => { logger.error('Failed to shutdown gracefully after unhandled rejection'); throw reason instanceof Error ? reason : new Error(String(reason)); }); }); // Export main function for CLI usage export { main }; // Start the server if this file is run directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { logger.error("Unhandled error: " + error); throw new Error(`Failed to start: ${error}`); }); } //# sourceMappingURL=index.js.map