Skip to main content
Glama
orchestrator.ts6.75 kB
#!/usr/bin/env node import { MCPSSHServer, MCPSSHServerConfig } from "./mcp-ssh-server.js"; import { WebServerManager, WebServerManagerConfig, } from "./web-server-manager.js"; import { SSHConnectionManager } from "./ssh-connection-manager.js"; import { PortManager } from "./port-discovery.js"; import { Logger } from "./logger.js"; import { TerminalSessionStateManager } from "./terminal-session-state-manager.js"; import * as fs from "fs"; import * as path from "path"; export interface OrchestratorConfig { webPort?: number; sshTimeout?: number; maxSessions?: number; logLevel?: string; } /** * Orchestrator - Manages separate MCP and Web servers together * Use this for manual server startup that includes both MCP and web interface */ export class Orchestrator { private mcpServer?: MCPSSHServer; private webServer?: WebServerManager; private sshManager: SSHConnectionManager; private portManager: PortManager; private config: OrchestratorConfig; private logger: Logger; private webPort?: number; private sharedStateManager: TerminalSessionStateManager; constructor(config: OrchestratorConfig = {}) { this.validateConfig(config); this.config = { webPort: config.webPort, sshTimeout: config.sshTimeout || 30000, maxSessions: config.maxSessions || 10, logLevel: config.logLevel || "info", ...config, }; this.portManager = new PortManager(); // Use file transport for orchestrator to avoid stdio pollution this.logger = new Logger('file', 'ORCHESTRATOR'); this.sshManager = new SSHConnectionManager(); // Create shared state manager FIRST this.sharedStateManager = new TerminalSessionStateManager(); // Note: MCP server will be created in start() method after web server is ready } private validateConfig(config: OrchestratorConfig): void { if ( config.webPort !== undefined && (config.webPort < 1 || config.webPort > 65535) ) { throw new Error("Invalid webPort: must be between 1 and 65535"); } if (config.sshTimeout !== undefined && config.sshTimeout < 1000) { throw new Error("Invalid sshTimeout: must be at least 1000ms"); } if (config.maxSessions !== undefined && config.maxSessions < 1) { throw new Error("Invalid maxSessions: must be at least 1"); } } async start(): Promise<void> { try { // Discover web port await this.discoverWebPort(); // Initialize web server with discovered port const webConfig: WebServerManagerConfig = { port: this.webPort!, }; this.webServer = new WebServerManager(this.sshManager, webConfig, this.sharedStateManager); // Start web server first to establish HTTP endpoint await this.webServer.start(); // Now create MCP server with web server reference for browser connection detection const mcpConfig: MCPSSHServerConfig = { sshTimeout: this.config.sshTimeout, maxSessions: this.config.maxSessions, logLevel: this.config.logLevel, }; this.mcpServer = new MCPSSHServer(mcpConfig, this.sshManager, this.sharedStateManager, this.webServer); // Configure servers with web port this.mcpServer.setWebServerPort(this.webPort!); this.sshManager.updateWebServerPort(this.webPort!); // Start MCP server with stdio transport (non-blocking) // Note: MCP server with stdio will block, so we start it last await this.mcpServer.start(); // Write port file for Claude Code MCP connection await this.writePortToFile(this.webPort!); // MCP SSH Server started - MCP: stdio, Web: ${this.webPort} } catch (error) { await this.cleanup(); throw error; } } private async discoverWebPort(): Promise<void> { if (this.config.webPort) { // Use specified port this.webPort = await this.portManager.reservePort(this.config.webPort); } else { // Auto-discover port starting from 8080 this.webPort = await this.portManager.getUnifiedPort(8080); } } private async writePortToFile(port: number): Promise<void> { const portFilePath = path.join(process.cwd(), ".ssh-mcp-server.port"); try { await fs.promises.writeFile(portFilePath, port.toString(), "utf8"); } catch (error) { this.logger.warn( `Could not write port to file ${portFilePath}: ${error instanceof Error ? error.message : String(error)}` ); } } private async removePortFile(): Promise<void> { const portFilePath = path.join(process.cwd(), ".ssh-mcp-server.port"); try { await fs.promises.unlink(portFilePath); } catch (error) { // Ignore error if file doesn't exist } } async stop(): Promise<void> { await this.cleanup(); } private async cleanup(): Promise<void> { const cleanupPromises: Promise<void>[] = []; // Stop web server if (this.webServer) { cleanupPromises.push(this.webServer.stop().catch(() => {})); } // Stop MCP server if (this.mcpServer) { cleanupPromises.push(this.mcpServer.stop().catch(() => {})); } // Cleanup SSH connections if (this.sshManager) { this.sshManager.cleanup(); } // Release port reservation if (this.webPort) { this.portManager.releasePort(this.webPort); } // Remove port file await this.removePortFile(); await Promise.all(cleanupPromises); } } // Main execution for orchestrator async function main(): Promise<void> { const config: OrchestratorConfig = { webPort: process.env.WEB_PORT ? parseInt(process.env.WEB_PORT) : undefined, sshTimeout: process.env.SSH_TIMEOUT ? parseInt(process.env.SSH_TIMEOUT) * 1000 : 30000, maxSessions: process.env.MAX_SESSIONS ? parseInt(process.env.MAX_SESSIONS) : 10, logLevel: process.env.LOG_LEVEL || "info", }; const orchestrator = new Orchestrator(config); // Handle graceful shutdown process.on("SIGINT", async () => { await orchestrator.stop(); process.exit(0); }); process.on("SIGTERM", async () => { await orchestrator.stop(); process.exit(0); }); try { await orchestrator.start(); } catch (error) { // CRITICAL: Use process.stderr.write to avoid stdio pollution process.stderr.write( `Failed to start orchestrator: ${error instanceof Error ? error.message : String(error)}\n` ); process.exit(1); } } if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { // CRITICAL: Use process.stderr.write to avoid stdio pollution process.stderr.write( `Unhandled error: ${error instanceof Error ? error.message : String(error)}\n` ); process.exit(1); }); }

Latest Blog Posts

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/LightspeedDMS/ssh-mcp'

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