UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

341 lines (340 loc) 15.8 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'; import { fileURLToPath } from 'url'; import logger, { registerShutdownCallback } from "./logger.js"; import { initializeToolEmbeddings } from './services/routing/embeddingStore.js'; import { OpenRouterConfigManager } from './utils/openrouter-config-manager.js'; import { ToolRegistry } from './services/routing/toolRegistry.js'; import { sseNotifier } from './services/sse-notifier/index.js'; import { transportManager } from './services/transport-manager/index.js'; import { PortAllocator } from './utils/port-allocator.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const envPath = path.resolve(__dirname, '../.env'); 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.`); } const isMessageHandlingTransport = (t) => t !== null && typeof t === 'object' && 'handlePostMessage' in t && typeof t.handlePostMessage === 'function'; const args = process.argv.slice(2); const useSSE = args.includes('--sse'); async function main(mcpServer) { try { if (useSSE) { const app = express(); app.use(cors()); app.use(express.json()); 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'); app.get('/health', (req, res) => { res.status(200).json({ status: 'ok' }); }); app.get('/sse', (req, res) => { const sessionId = req.query.sessionId || `sse-${Math.random().toString(36).substring(2)}`; const transport = new SSEServerTransport('/messages', res); req.sessionId = sessionId; logger.info({ sessionId, transportSessionId: transport.sessionId }, 'Established SSE connection'); mcpServer.connect(transport).catch((error) => { logger.error({ err: error }, 'Failed to connect transport'); }); }); app.post('/messages', async (req, res) => { if (!req.body) { return res.status(400).json({ error: 'Invalid request body' }); } try { const sessionId = req.query.sessionId || req.body.session_id; if (!sessionId) { return res.status(400).json({ error: 'Missing session ID. Establish an SSE connection first.' }); } const transport = mcpServer.server.transport; if (!transport) { return res.status(400).json({ error: 'No active SSE connection' }); } if (isMessageHandlingTransport(transport)) { 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'); await PortAllocator.registerInstance(port, 'sse-server'); }); app.get('/events/:sessionId', (req, res) => { 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); }); } else { process.env.MCP_TRANSPORT = 'stdio'; console.log = (...args) => process.stderr.write(args.join(' ') + '\n'); console.info = (...args) => process.stderr.write('[INFO] ' + args.join(' ') + '\n'); console.warn = (...args) => process.stderr.write('[WARN] ' + args.join(' ') + '\n'); console.error = (...args) => process.stderr.write('[ERROR] ' + args.join(' ') + '\n'); const stdioSessionId = 'stdio-session'; const transport = new StdioServerTransport(); logger.info({ sessionId: stdioSessionId }, 'Initialized stdio transport with session ID'); await mcpServer.connect(transport); logger.info('Vibe Coder MCP server running on stdio'); } } catch (error) { logger.fatal({ err: error }, 'Server error'); process.exit(1); } } async function initDirectories() { try { 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'); } } async function initializeApp() { const cwd = process.cwd(); const scriptDir = __dirname; const projectRoot = path.resolve(scriptDir, '..'); logger.info({ cwd, scriptDir, projectRoot }, 'Directory information'); 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'); } logger.info('Initializing centralized OpenRouter configuration manager...'); const configManager = OpenRouterConfigManager.getInstance(); await configManager.initialize(); const openRouterConfig = await configManager.getOpenRouterConfig(); 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 }); 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'); } logger.info('Initializing ToolRegistry with full configuration including model mappings'); ToolRegistry.getInstance(openRouterConfig); await initializeToolEmbeddings(); 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]; 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'); } 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'); } transportManager.configure({ websocket: { enabled: true, port: 8080, path: '/agent-ws' }, http: { enabled: true, port: 3011, cors: true }, sse: { enabled: true }, stdio: { enabled: true } }); 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'); } 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 openRouterConfig; } initializeApp().then(async (loadedConfig) => { const { createServer } = await import('./server.js'); const server = createServer(loadedConfig); logger.info('Initializing tool directories after server creation...'); await initDirectories(); const instanceDir = process.env.VIBE_CODER_INSTANCE_DIR; const outputDir = process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'); let instanceTrackingDir; if (instanceDir) { instanceTrackingDir = instanceDir; logger.info({ instanceDir }, 'Using explicitly configured instance directory'); } else if (process.env.VIBE_CODER_OUTPUT_DIR) { instanceTrackingDir = path.join(outputDir, '.temp', 'instances'); logger.info({ instanceTrackingDir }, 'Using instance directory within configured output directory'); } if (instanceTrackingDir) { PortAllocator.initialize({ instanceTrackingDir }); } else { logger.debug('PortAllocator using default OS temp instance tracking directory'); PortAllocator.initialize({}); } registerShutdownCallback(async () => { logger.info('Cleaning up port allocations...'); await PortAllocator.cleanupInstanceTracking(); }); main(server).catch(error => { logger.fatal({ err: error }, 'Failed to start server'); process.exit(1); }); }).catch(initError => { logger.fatal({ err: initError }, 'Failed during application initialization'); process.exit(1); });