Skip to main content
Glama

Vibe Coder MCP

by freshtechbro
index.ts19.4 kB
#!/usr/bin/env node import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; import cors from "cors"; import dotenv from "dotenv"; import path from 'path'; // Ensure path is imported import { fileURLToPath } from 'url'; // Needed for ES Module path resolution import logger, { registerShutdownCallback } from "./logger.js"; import { initializeToolEmbeddings } from './services/routing/embeddingStore.js'; // Removed unused imports import { OpenRouterConfigManager } from './utils/openrouter-config-manager.js'; import { ToolRegistry } from './services/routing/toolRegistry.js'; // Import ToolRegistry to initialize it properly import { sseNotifier } from './services/sse-notifier/index.js'; // Import the SSE notifier singleton import { transportManager } from './services/transport-manager/index.js'; // Import transport manager singleton import { PortAllocator } from './utils/port-allocator.js'; // Import port allocator for cleanup // Note: createServer will be dynamically imported after ToolRegistry initialization // to ensure proper tool registration timing // --- Load .env file explicitly --- // Get the directory name of the current module (build/index.js) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Construct the path to the .env file in the project root (one level up from build) const envPath = path.resolve(__dirname, '../.env'); // Load environment variables from the specific path const dotenvResult = dotenv.config({ path: envPath }); if (dotenvResult.error) { logger.warn({ err: dotenvResult.error, path: envPath }, `Could not load .env file from explicit path. Environment variables might be missing.`); } else { logger.info({ path: envPath, loaded: dotenvResult.parsed ? Object.keys(dotenvResult.parsed) : [] }, `Loaded environment variables from .env file.`); } // --- End .env loading --- // Define an interface for transports that handle POST messages interface TransportWithMessageHandling { handlePostMessage(req: express.Request, res: express.Response, context?: Record<string, unknown>): Promise<void>; // Add other common transport properties/methods if needed, e.g., from SSEServerTransport } // Type guard to check if an object conforms to TransportWithMessageHandling const isMessageHandlingTransport = (t: unknown): t is TransportWithMessageHandling => t !== null && typeof t === 'object' && 'handlePostMessage' in t && typeof (t as TransportWithMessageHandling).handlePostMessage === 'function'; // Determine transport based on command line arguments const args = process.argv.slice(2); const useSSE = args.includes('--sse'); // Define main function *before* it's called async function main(mcpServer: import("@modelcontextprotocol/sdk/server/mcp.js").McpServer) { try { if (useSSE) { // Set up Express server for SSE with dynamic port allocation const app = express(); app.use(cors()); app.use(express.json()); // Get allocated SSE port from Transport Manager, fallback to environment or default const allocatedSsePort = transportManager.getServicePort('sse'); const port = allocatedSsePort || (process.env.SSE_PORT ? parseInt(process.env.SSE_PORT) : undefined) || (process.env.PORT ? parseInt(process.env.PORT) : 3000); logger.debug({ allocatedSsePort, envSsePort: process.env.SSE_PORT, envPort: process.env.PORT, finalPort: port }, 'SSE server port selection'); // Add a health endpoint app.get('/health', (req: express.Request, res: express.Response) => { res.status(200).json({ status: 'ok' }); }); app.get('/sse', (req: express.Request, res: express.Response) => { // Extract session ID from query parameters or generate a new one const sessionId = req.query.sessionId as string || `sse-${Math.random().toString(36).substring(2)}`; // Create a transport const transport = new SSEServerTransport('/messages', res); // Store the session ID in the request object for later use (req as express.Request & { sessionId?: string }).sessionId = sessionId; // Log the session ID logger.info({ sessionId, transportSessionId: transport.sessionId }, 'Established SSE connection'); // Store the session ID in a global map for later use // sseNotifier.registerConnection(sessionId, res); // Connect the transport to the server mcpServer.connect(transport).catch((error: Error) => { logger.error({ err: error }, 'Failed to connect transport'); }); }); app.post('/messages', async (req: express.Request, res: express.Response) => { if (!req.body) { return res.status(400).json({ error: 'Invalid request body' }); } try { // Extract session ID from query parameters or body const sessionId = req.query.sessionId as string || req.body.session_id; if (!sessionId) { return res.status(400).json({ error: 'Missing session ID. Establish an SSE connection first.' }); } // Find the active transport for this session const transport = mcpServer.server.transport; if (!transport) { return res.status(400).json({ error: 'No active SSE connection' }); } if (isMessageHandlingTransport(transport)) { // Pass the session ID and transport type in the context const context = { sessionId, transportType: sessionId === 'stdio-session' ? 'stdio' : 'sse' }; await transport.handlePostMessage(req, res, context); } else { logger.error('Active transport does not support handlePostMessage or is not defined.'); if (!res.headersSent) { res.status(500).json({ error: 'Internal server error: Cannot handle POST message.' }); } return; } } catch (error) { logger.error({ err: error }, 'Error handling POST message'); if (!res.headersSent) { res.status(500).json({ error: 'Internal server error while handling POST message.' }); } } }); app.listen(port, async () => { logger.info({ port, allocatedByTransportManager: !!allocatedSsePort, source: allocatedSsePort ? 'Transport Manager' : 'Environment/Default' }, `Vibe Coder MCP SSE server running on http://localhost:${port}`); logger.info('Connect using SSE at /sse and post messages to /messages'); logger.info('Subscribe to job progress events at /events/:sessionId'); // Log new endpoint // Register the instance with port tracking await PortAllocator.registerInstance(port, 'sse-server'); }); // --- Add new SSE endpoint for job progress --- app.get('/events/:sessionId', (req: express.Request, res: express.Response) => { const sessionId = req.params.sessionId; if (!sessionId) { res.status(400).send('Session ID is required.'); return; } logger.info({ sessionId }, `Received request to establish SSE connection for job progress.`); sseNotifier.registerConnection(sessionId, res); }); // --- End new SSE endpoint --- } else { // Set environment variable to indicate stdio transport is being used process.env.MCP_TRANSPORT = 'stdio'; // Override console methods to prevent stdout contamination in stdio mode // Redirect all console output to stderr when using stdio transport console.log = (...args: unknown[]) => process.stderr.write(args.join(' ') + '\n'); console.info = (...args: unknown[]) => process.stderr.write('[INFO] ' + args.join(' ') + '\n'); console.warn = (...args: unknown[]) => process.stderr.write('[WARN] ' + args.join(' ') + '\n'); console.error = (...args: unknown[]) => process.stderr.write('[ERROR] ' + args.join(' ') + '\n'); // Use stdio transport with session ID const stdioSessionId = 'stdio-session'; const transport = new StdioServerTransport(); // Log the session ID (this will now go to stderr due to our logger fix) logger.info({ sessionId: stdioSessionId }, 'Initialized stdio transport with session ID'); // We'll pass the session ID and transport type in the context when handling messages await mcpServer.connect(transport); // Use mcpServer logger.info('Vibe Coder MCP server running on stdio'); } } catch (error) { logger.fatal({ err: error }, 'Server error'); process.exit(1); } } // Initialize all tool directories async function initDirectories() { try { // Using dynamic imports with try/catch to handle missing files gracefully try { const researchManager = await import('./tools/research-manager/index.js'); if (typeof researchManager.initDirectories === 'function') { await researchManager.initDirectories(); logger.debug('Initialized research-manager directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing research-manager'); } try { const rulesGenerator = await import('./tools/rules-generator/index.js'); if (typeof rulesGenerator.initDirectories === 'function') { await rulesGenerator.initDirectories(); logger.debug('Initialized rules-generator directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing rules-generator'); } try { const prdGenerator = await import('./tools/prd-generator/index.js'); if (typeof prdGenerator.initDirectories === 'function') { await prdGenerator.initDirectories(); logger.debug('Initialized prd-generator directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing prd-generator'); } try { const userStoriesGenerator = await import('./tools/user-stories-generator/index.js'); if (typeof userStoriesGenerator.initDirectories === 'function') { await userStoriesGenerator.initDirectories(); logger.debug('Initialized user-stories-generator directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing user-stories-generator'); } try { const contextCurator = await import('./tools/context-curator/index.js'); if (typeof contextCurator.initDirectories === 'function') { await contextCurator.initDirectories(); logger.debug('Initialized context-curator directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing context-curator'); } try { const taskListGenerator = await import('./tools/task-list-generator/index.js'); if (typeof taskListGenerator.initDirectories === 'function') { await taskListGenerator.initDirectories(); logger.debug('Initialized task-list-generator directories'); } } catch (error) { logger.error({ err: error }, 'Error initializing task-list-generator'); } logger.info('Tool directory initialization complete'); } catch (error) { logger.error({ err: error }, 'Error initializing directories'); } } // New function to handle all async initialization steps async function initializeApp() { // Log current directories for debugging const cwd = process.cwd(); const scriptDir = __dirname; const projectRoot = path.resolve(scriptDir, '..'); logger.info({ cwd, scriptDir, projectRoot }, 'Directory information'); // Just warn if working directory is root, don't fail // MCP clients often start servers from root directory if (cwd === '/' || cwd === '\\' || cwd === 'C:\\' || cwd === 'C:/' || cwd.match(/^[A-Z]:[\\/]$/)) { logger.warn({ cwd, scriptDir, projectRoot, message: 'Working directory is root. Using project root for relative paths.' }, 'Working directory warning'); } // Initialize centralized OpenRouter configuration manager logger.info('Initializing centralized OpenRouter configuration manager...'); const configManager = OpenRouterConfigManager.getInstance(); await configManager.initialize(); // Get OpenRouter configuration from centralized manager const openRouterConfig = await configManager.getOpenRouterConfig(); // Log the loaded configuration details const mappingKeys = Object.keys(openRouterConfig.llm_mapping || {}); logger.info('Loaded OpenRouter configuration details:', { hasApiKey: Boolean(openRouterConfig.apiKey), baseUrl: openRouterConfig.baseUrl, geminiModel: openRouterConfig.geminiModel, perplexityModel: openRouterConfig.perplexityModel, mappingLoaded: mappingKeys.length > 0, numberOfMappings: mappingKeys.length, mappingKeys: mappingKeys }); // Validate configuration const validation = configManager.validateConfiguration(); if (!validation.valid) { logger.error({ errors: validation.errors }, 'OpenRouter configuration validation failed'); throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`); } if (validation.warnings.length > 0) { logger.warn({ warnings: validation.warnings, suggestions: validation.suggestions }, 'OpenRouter configuration has warnings'); } // CRITICAL - Initialize the ToolRegistry with the proper config BEFORE any tools are registered // This ensures all tools will receive the correct config with llm_mapping intact logger.info('Initializing ToolRegistry with full configuration including model mappings'); ToolRegistry.getInstance(openRouterConfig); // Now that the registry is initialized with the proper config, we can safely load tools // which will register themselves with the properly configured registry // NOTE: initDirectories() moved to after server creation to ensure UnifiedSecurityConfigManager is initialized await initializeToolEmbeddings(); // Initialize embeddings // Check for other running vibe-coder-mcp instances try { logger.info('Checking for other running vibe-coder-mcp instances...'); const commonPorts = [8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 3011, 3012]; // Use the new conflict detection method const conflicts = await PortAllocator.detectPortConflicts(commonPorts); const portsInUse = Array.from(conflicts.entries()) .filter(([_, inUse]) => inUse) .map(([port, _]) => port); if (portsInUse.length > 0) { logger.warn({ portsInUse, conflictCount: portsInUse.length, message: 'Detected ports in use that may indicate other vibe-coder-mcp instances running' }, 'Multiple instance detection warning'); } else { logger.info('No conflicting instances detected on common ports'); } } catch (error) { logger.warn({ err: error }, 'Instance detection failed, continuing with startup'); } // Cleanup orphaned ports from previous crashed instances try { logger.info('Starting port cleanup for orphaned processes...'); const cleanedPorts = await PortAllocator.cleanupOrphanedPorts(); logger.info({ cleanedPorts }, 'Port cleanup completed'); } catch (error) { logger.warn({ err: error }, 'Port cleanup failed, continuing with startup'); } // Configure transport services with dynamic port allocation // Enable all transports for comprehensive agent communication transportManager.configure({ websocket: { enabled: true, port: 8080, path: '/agent-ws' }, http: { enabled: true, port: 3011, cors: true }, sse: { enabled: true }, stdio: { enabled: true } }); // Start transport services for agent communication using coordinator try { const { transportCoordinator } = await import('./services/transport-coordinator.js'); await transportCoordinator.ensureTransportsStarted(); logger.info('All transport services started successfully with dynamic port allocation'); } catch (error) { logger.error({ err: error }, 'Failed to start transport services'); // Don't throw - allow application to continue with available transports } // Register shutdown callbacks for graceful cleanup registerShutdownCallback(async () => { logger.info('Shutting down transport services...'); try { await transportManager.stopAll(); logger.info('Transport services stopped successfully'); } catch (error) { logger.error({ err: error }, 'Error stopping transport services'); } }); registerShutdownCallback(async () => { logger.info('Cleaning up port allocations...'); try { await PortAllocator.cleanupOrphanedPorts(); logger.info('Port cleanup completed'); } catch (error) { logger.error({ err: error }, 'Error during port cleanup'); } }); logger.info('Application initialization complete.'); // Return the fully loaded config return openRouterConfig; } // Initialize app, create server with loaded config, then start main logic initializeApp().then(async (loadedConfig) => { // Dynamically import createServer AFTER ToolRegistry initialization and tool imports const { createServer } = await import('./server.js'); const server = createServer(loadedConfig); // Pass loaded config to server creation // Initialize tool directories AFTER server creation to ensure UnifiedSecurityConfigManager is initialized logger.info('Initializing tool directories after server creation...'); await initDirectories(); // Initialize PortAllocator with configured instance directory const instanceDir = process.env.VIBE_CODER_INSTANCE_DIR; const outputDir = process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'); // Determine the instance tracking directory let instanceTrackingDir: string | undefined; if (instanceDir) { // Use explicit instance directory if provided instanceTrackingDir = instanceDir; logger.info({ instanceDir }, 'Using explicitly configured instance directory'); } else if (process.env.VIBE_CODER_OUTPUT_DIR) { // If output directory is configured, use a subdirectory within it instanceTrackingDir = path.join(outputDir, '.temp', 'instances'); logger.info({ instanceTrackingDir }, 'Using instance directory within configured output directory'); } // Initialize PortAllocator if (instanceTrackingDir) { PortAllocator.initialize({ instanceTrackingDir }); } else { logger.debug('PortAllocator using default OS temp instance tracking directory'); PortAllocator.initialize({}); } // Register shutdown callback to clean up port tracking registerShutdownCallback(async () => { logger.info('Cleaning up port allocations...'); await PortAllocator.cleanupInstanceTracking(); }); main(server).catch(error => { // Pass server instance to main logger.fatal({ err: error }, 'Failed to start server'); process.exit(1); }); }).catch(initError => { logger.fatal({ err: initError }, 'Failed during application initialization'); process.exit(1); });

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/freshtechbro/vibe-coder-mcp'

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