UNPKG

@civic/hub-bridge

Version:

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

192 lines 8.53 kB
// Hub Bridge - MCP Bridge with Civic Auth // This is a rewritten version of the hub-bridge package import { createPassthroughProxy } from "@civic/passthrough-mcp-server"; import { getConfig } from "./config/index.js"; import { AuthenticationHook, SessionRecoveryHook } from "./hooks/index.js"; import { CLIAuthProviderSingleton } from "./auth/cli-auth-provider-singleton.js"; import { localTools } from './tools/index.js'; import { z } from 'zod'; import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import * as configVars from './config/index.js'; import { handleCliArguments, getVersion } from './cli/index.js'; import { logger } from "./utils/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 proxyConfig = getConfig(); logger.info("Config loaded:", JSON.stringify(proxyConfig, null, 2)); try { // Get the singleton auth provider const authProvider = CLIAuthProviderSingleton.getInstance(); proxy = await createPassthroughProxy({ ...proxyConfig, hooks: [ new AuthenticationHook(authProvider), new SessionRecoveryHook() ], autoStart: false }); // Add local tools to the FastMCP server for (const localTool of localTools) { // Convert JSON Schema properties to Zod schema const zodProperties = {}; if (localTool.inputSchema.properties) { for (const [key, prop] of Object.entries(localTool.inputSchema.properties)) { const property = prop; if (property.type === 'string') { zodProperties[key] = z.string(); } else if (property.type === 'number') { zodProperties[key] = z.number(); } else if (property.type === 'boolean') { zodProperties[key] = z.boolean(); } // Add more types as needed } } 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 ToolCallResult 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(); // 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 MCP Server running with ${proxyConfig.transportType} transport ${proxyConfig.transportType !== "stdio" ? ` on port ${proxyConfig.port}` : ""}, connecting to target at ${proxyConfig.target.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