Skip to main content
Glama

Interactive Brokers MCP Server

by code-rabi
gateway-manager.ts15.3 kB
import { spawn, ChildProcess } from 'child_process'; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import { Logger } from './logger.js'; import { PortUtils } from './utils/port-utils.js'; import { ConfigUtils } from './utils/config-utils.js'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); export class IBGatewayManager { private gatewayProcess: ChildProcess | null = null; private gatewayDir: string; private jreDir: string; private isStarting = false; private isReady = false; private useStderr: boolean; private cleanupHandlersRegistered = false; private currentPort: number = 5000; private backgroundStartupPromise: Promise<void> | null = null; constructor() { this.gatewayDir = path.join(__dirname, '../ib-gateway'); this.jreDir = path.join(__dirname, '../runtime'); this.useStderr = !(process.env.MCP_HTTP_SERVER === 'true' || process.argv.includes('--http')); this.registerCleanupHandlers(); } private log(message: string) { Logger.info(message); } private async findExistingGateway(): Promise<number | null> { this.log('🔍 Checking for existing Gateway instances...'); const existingPort = await PortUtils.findExistingGateway(); if (existingPort) { this.log(`✅ Found existing Gateway on port ${existingPort}`); } else { this.log('🚫 No existing Gateway found'); } return existingPort; } async quickCheckExistingGateway(): Promise<number | null> { this.log('⚡ Quick check for existing Gateway instances...'); try { const existingPort = await PortUtils.findExistingGateway(); if (existingPort) { this.log(`✅ Found existing Gateway on port ${existingPort}`); } else { this.log('⚡ Quick check complete - no existing Gateway found'); } return existingPort; } catch (error) { this.log('⚡ Quick check failed, continuing...'); return null; } } private registerCleanupHandlers(): void { if (this.cleanupHandlersRegistered) { return; } this.cleanupHandlersRegistered = true; // Handle graceful shutdown signals - only clean temp files, don't kill gateway const cleanup = async (signal: string) => { this.log(`🛑 Received ${signal}, cleaning up temp files only...`); await this.cleanup(); process.exit(0); }; // Handle different termination signals process.on('SIGINT', () => cleanup('SIGINT')); process.on('SIGTERM', () => cleanup('SIGTERM')); process.on('SIGHUP', () => cleanup('SIGHUP')); // Handle uncaught exceptions and unhandled rejections process.on('uncaughtException', async (error) => { Logger.error('❌ Uncaught Exception:', error); await this.cleanup(); process.exit(1); }); process.on('unhandledRejection', async (reason, promise) => { Logger.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); await this.cleanup(); process.exit(1); }); // Handle normal process exit - only clean temp files process.on('exit', (code) => { this.log(`🛑 Process exiting with code ${code}, cleaning temp files only...`); // Don't kill gateway - just clean references this.gatewayProcess = null; this.isReady = false; this.isStarting = false; }); // Handle when parent process dies (useful for child processes) process.on('disconnect', async () => { this.log('🛑 Parent process disconnected, cleaning up temp files only...'); await this.cleanup(); process.exit(0); }); } private async cleanup(): Promise<void> { try { // Only clean up temporary config files - don't kill gateway this.log('🧹 Cleaning up temporary files only...'); await ConfigUtils.cleanupTempConfigFiles(this.gatewayDir); // Just clear references without killing the process this.gatewayProcess = null; this.isReady = false; this.isStarting = false; } catch (error) { Logger.error('❌ Error during cleanup:', error); // Still don't kill gateway - just clear references this.gatewayProcess = null; this.isReady = false; this.isStarting = false; } } // Removed forceKillGateway - we never kill gateway processes anymore private getJavaPath(): string { const platform = `${process.platform}-${process.arch}`; const isWindows = process.platform === 'win32'; const javaExecutable = isWindows ? 'java.exe' : 'java'; const runtimePath = path.join(this.jreDir, platform, 'bin', javaExecutable); if (!require('fs').existsSync(runtimePath)) { throw new Error(`Custom runtime not found for platform: ${platform}. Expected at: ${runtimePath}`); } return runtimePath; } async ensureGatewayExists(): Promise<void> { const gatewayPath = path.join(this.gatewayDir, 'clientportal.gw'); const runScript = path.join(gatewayPath, 'bin/run.sh'); try { await fs.access(runScript); this.log('✅ IB Gateway found at:' + gatewayPath); } catch { throw new Error(`IB Gateway not found at ${gatewayPath}. Please ensure the gateway files are properly installed.`); } } // Public method for fast initialization (used during server startup) async quickStartGateway(): Promise<void> { this.log('⚡ Quick Gateway initialization...'); // Quick check for existing Gateway (aggressive timeouts) const existingPort = await this.quickCheckExistingGateway(); if (existingPort) { this.currentPort = existingPort; this.isReady = true; this.log(`✅ Using existing Gateway on port ${existingPort}`); return; } // No existing Gateway - start new one in background this.log('🚀 No existing Gateway found - starting new one in background...'); this.startGatewayAsync(); } // Start Gateway in background (non-blocking) startGatewayAsync(): void { if (this.backgroundStartupPromise) { this.log('Background Gateway startup already in progress'); return; } // Wrap the startup in a promise that handles errors gracefully this.backgroundStartupPromise = (async () => { try { await this.startGatewayInternal(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`❌ Background Gateway startup failed: ${errorMessage}`); // Reset the promise so sync startup can be attempted later this.backgroundStartupPromise = null; throw error; } })(); // Add unhandled rejection handler to prevent process termination this.backgroundStartupPromise.catch((error) => { // Error already logged above, just prevent unhandled rejection }); } // Ensure Gateway is ready (used by tool handlers) async ensureGatewayReady(): Promise<void> { if (this.isReady) { return; // Already ready } this.log('⏳ Tool called - ensuring Gateway is ready...'); // First, try to find existing Gateway again (might have started since init) const existingPort = await this.findExistingGateway(); if (existingPort) { this.currentPort = existingPort; this.isReady = true; this.log(`✅ Found existing Gateway on port ${existingPort}`); return; } // Wait for background startup if it's running if (this.backgroundStartupPromise) { this.log('⏳ Waiting for background Gateway startup to complete...'); try { await this.backgroundStartupPromise; if (this.isReady) { return; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`⚠️ Background startup failed, attempting synchronous start: ${errorMessage}`); } } // If no background startup or it failed, start synchronously this.log('⏳ Starting Gateway synchronously...'); await this.startGatewayInternal(); } // Backwards compatibility - redirect to quickStartGateway async startGateway(): Promise<void> { await this.quickStartGateway(); } private async startGatewayInternal(): Promise<void> { if (this.isStarting || this.isReady) { this.log('Gateway is already starting or ready'); return; } this.isStarting = true; try { await this.ensureGatewayExists(); // Check port availability for new Gateway this.log('🔍 Checking port availability for new Gateway...'); const defaultPort = 5000; if (await PortUtils.isPortAvailable(defaultPort)) { this.currentPort = defaultPort; this.log(`✅ Using default port ${defaultPort}`); } else { this.log(`❌ Default port ${defaultPort} is occupied, trying to find alternative...`); try { this.currentPort = await PortUtils.findAvailablePort(5001, 9); // Try 5001-5009 this.log(`✅ Found alternative port ${this.currentPort}`); // Create a temporary config file with the new port await ConfigUtils.createTempConfigWithPort(this.gatewayDir, this.currentPort); this.log(`📝 Created temporary config file with port ${this.currentPort}`); } catch (error) { this.log(`❌ No alternative ports available, will try with default port anyway`); this.currentPort = defaultPort; } } const bundledJavaPath = this.getJavaPath(); const bundledJavaHome = path.dirname(path.dirname(bundledJavaPath)); const bundledJavaLibPath = path.join(bundledJavaHome, 'lib'); const bundledJavaServerLibPath = path.join(bundledJavaLibPath, 'server'); const configFile = this.currentPort === defaultPort ? 'root/conf.yaml' : `root/conf-${this.currentPort}.yaml`; const jarPath = path.join(this.gatewayDir, 'clientportal.gw/dist/ibgroup.web.core.iblink.router.clientportal.gw.jar'); const runtimePath = path.join(this.gatewayDir, 'clientportal.gw/build/lib/runtime/*'); const configDir = path.join(this.gatewayDir, 'clientportal.gw/root'); const classpath = `${configDir}:${jarPath}:${runtimePath}`; this.log('🚀 Starting IB Gateway with bundled JRE...'); this.log(' Java: ' + bundledJavaPath); this.log(' Java Home: ' + bundledJavaHome); this.log(' Lib Path: ' + `${bundledJavaServerLibPath}:${bundledJavaLibPath}`); this.log(' Config: ' + configFile); this.log(' Port: ' + this.currentPort); this.gatewayProcess = spawn(bundledJavaPath, [ '-server', '-Djava.awt.headless=true', '-Xmx512m', '-Dvertx.disableDnsResolver=true', '-Djava.net.preferIPv4Stack=true', '-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory', '-Dnologback.statusListenerClass=ch.qos.logback.core.status.OnConsoleStatusListener', '-Dnolog4j.debug=true', '-Dnolog4j2.debug=true', '-cp', classpath, 'ibgroup.web.core.clientportal.gw.GatewayStart', '--conf', `../${configFile}` ], { cwd: path.join(this.gatewayDir, 'clientportal.gw'), env: { ...process.env, JAVA_HOME: bundledJavaHome, LD_LIBRARY_PATH: `${bundledJavaServerLibPath}:${bundledJavaLibPath}:${process.env.LD_LIBRARY_PATH || ''}` }, stdio: ['ignore', 'pipe', 'pipe'] }); this.gatewayProcess.stdout?.on('data', (data) => { const output = data.toString().trim(); if (output) { this.log(`[Gateway] ${output}`); // Check for startup completion indicators if (output.includes('Server ready') || output.includes('started on port')) { this.isReady = true; this.log('✅ IB Gateway is ready!'); } } }); this.gatewayProcess.stderr?.on('data', (data) => { const output = data.toString().trim(); if (output && !output.includes('WARNING')) { Logger.error(`[Gateway Error] ${output}`); } }); this.gatewayProcess.on('error', (error) => { Logger.error('❌ Gateway process error:', error.message); this.isStarting = false; this.isReady = false; }); this.gatewayProcess.on('exit', (code, signal) => { this.log(`🛑 Gateway process exited with code ${code}, signal ${signal}`); this.gatewayProcess = null; this.isStarting = false; this.isReady = false; }); // Wait for the gateway to be ready this.log('⏳ Waiting for IB Gateway to start...'); await this.waitForGateway(); this.isStarting = false; this.isReady = true; this.log('🎉 IB Gateway started successfully!'); } catch (error) { this.isStarting = false; this.isReady = false; throw error; } } private async waitForGateway(): Promise<void> { const maxAttempts = 30; // 30 seconds let attempts = 0; while (attempts < maxAttempts) { try { // Try to connect to the gateway port const response = await this.checkGatewayHealth(); if (response) { this.log(`✅ IB Gateway is responding on port ${this.currentPort}`); return; } } catch (error) { // Gateway not ready yet, continue waiting } attempts++; await new Promise(resolve => setTimeout(resolve, 1000)); if (attempts % 5 === 0) { this.log(`⏳ Still waiting for gateway... (${attempts}/${maxAttempts})`); } } throw new Error('IB Gateway failed to start within 30 seconds'); } private async checkGatewayHealth(): Promise<boolean> { // Import https dynamically to avoid issues with module resolution const https = await import('https'); return new Promise((resolve, reject) => { const options = { hostname: 'localhost', port: this.currentPort, path: '/', method: 'GET', rejectUnauthorized: false, // Accept self-signed certificates timeout: 5000 }; const req = https.request(options, (res) => { resolve(res.statusCode === 200 || res.statusCode === 401 || res.statusCode === 302); }); req.on('error', () => { resolve(false); }); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); }); } async stopGateway(): Promise<void> { // Don't actually stop the gateway - just disconnect from it this.log('🔗 Disconnecting from IB Gateway (leaving it running)...'); this.gatewayProcess = null; this.isReady = false; this.isStarting = false; this.log('✅ Disconnected from IB Gateway'); } isGatewayReady(): boolean { return this.isReady && this.gatewayProcess !== null; } getGatewayUrl(): string { return `https://localhost:${this.currentPort}`; } getCurrentPort(): number { return this.currentPort; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/code-rabi/interactive-brokers-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server