UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

328 lines (327 loc) 13.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const dotenv_1 = __importDefault(require("dotenv")); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const zod_1 = require("zod"); // We'll need this for tool schemas const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); // --- Core Logic Modules --- const historyLogger_js_1 = require("./modules/historyLogger.js"); const codebaseAnalyzer_js_1 = require("./modules/codebaseAnalyzer.js"); const confluenceReader_js_1 = require("./modules/confluenceReader.js"); const featureSpecProcessor_js_1 = require("./modules/featureSpecProcessor.js"); const taskManager_js_1 = require("./modules/taskManager.js"); // Load environment variables from .env file dotenv_1.default.config(); // --- Configuration --- const PORT = process.env.PORT || 3001; const SERVER_NAME = 'Lamplighter-MCP'; const SERVER_VERSION = '1.0.0'; const CONTEXT_DIR = process.env.LAMPLIGHTER_CONTEXT_DIR || './lamplighter_context'; // File paths const codebaseSummaryPath = path.join(CONTEXT_DIR, 'codebase_summary.md'); const historyLogPath = path.join(CONTEXT_DIR, 'history_log.md'); const featureTasksDir = path.join(CONTEXT_DIR, 'feature_tasks'); // --- MCP Server Setup --- const server = new mcp_js_1.McpServer({ name: SERVER_NAME, version: SERVER_VERSION, }); // --- Core Logic Modules (Instantiation) --- const historyLogger = new historyLogger_js_1.HistoryLogger(); const codebaseAnalyzer = new codebaseAnalyzer_js_1.CodebaseAnalyzer(); const confluenceReader = new confluenceReader_js_1.ConfluenceReader(); const featureSpecProcessor = new featureSpecProcessor_js_1.FeatureSpecProcessor(); const taskManager = new taskManager_js_1.TaskManager(); // Helper function to derive a feature ID from a URL function deriveFeatureId(url) { return featureSpecProcessor.deriveFeatureId(url); } // --- MCP Tools --- // 1. analyze_codebase tool server.tool('analyze_codebase', 'Analyzes the current codebase structure and generates a summary', {}, // Empty schema since we don't need any parameters async () => { try { await codebaseAnalyzer.analyze(process.cwd()); await historyLogger.log('Codebase analysis completed successfully'); return { content: [{ type: 'text', text: 'Codebase analysis completed. Check codebase_summary.md for results.' }] }; } catch (error) { console.error('[analyze_codebase] Error:', error); return { content: [{ type: 'text', text: `Error analyzing codebase: ${error}` }], isError: true }; } }); // 2. process_confluence_spec tool server.tool('process_confluence_spec', 'Processes a Confluence specification into actionable tasks', { confluence_url: zod_1.z.string().url() }, async ({ confluence_url }) => { try { // Fetch content from Confluence const specText = await confluenceReader.fetchPageContent(confluence_url); // Read the codebase summary for context let codebaseSummary; try { codebaseSummary = await fs.readFile(codebaseSummaryPath, 'utf-8'); } catch (error) { // If summary doesn't exist, run the analyzer first await codebaseAnalyzer.analyze(process.cwd()); codebaseSummary = await fs.readFile(codebaseSummaryPath, 'utf-8'); } // Derive feature ID from URL const featureId = deriveFeatureId(confluence_url); // Process the specification into tasks const filePath = await featureSpecProcessor.processSpecification(specText, codebaseSummary, featureId); // Log the action await historyLogger.log(`Processed specification from ${confluence_url} as feature "${featureId}"`); return { content: [{ type: 'text', text: `Specification processed successfully. Tasks available at ${filePath}` }] }; } catch (error) { console.error('[process_confluence_spec] Error:', error); return { content: [{ type: 'text', text: `Error processing specification: ${error}` }], isError: true }; } }); // 3. update_task_status tool server.tool('update_task_status', 'Updates the status of a specific task within a feature', { feature_identifier: zod_1.z.string(), task_identifier: zod_1.z.string(), new_status: zod_1.z.enum(['ToDo', 'InProgress', 'Done']) }, async ({ feature_identifier, task_identifier, new_status }) => { try { await taskManager.updateTaskStatus(feature_identifier, task_identifier, new_status); await historyLogger.log(`Updated task "${task_identifier}" in feature "${feature_identifier}" to ${new_status}`); return { content: [{ type: 'text', text: `Task status updated to ${new_status}` }] }; } catch (error) { console.error('[update_task_status] Error:', error); return { content: [{ type: 'text', text: `Error updating task status: ${error}` }], isError: true }; } }); // 4. suggest_next_task tool server.tool('suggest_next_task', 'Identifies and returns the next actionable task from a feature', { feature_identifier: zod_1.z.string() }, async ({ feature_identifier }) => { try { const nextTask = await taskManager.suggestNextTask(feature_identifier); if (nextTask) { return { content: [{ type: 'text', text: `Next task: ${nextTask}` }] }; } else { return { content: [{ type: 'text', text: `No pending tasks found for feature "${feature_identifier}".` }] }; } } catch (error) { console.error('[suggest_next_task] Error:', error); return { content: [{ type: 'text', text: `Error suggesting next task: ${error}` }], isError: true }; } }); // 5. get_codebase_summary tool server.tool('get_codebase_summary', 'Retrieves the codebase summary', {}, async () => { try { const content = await fs.readFile(codebaseSummaryPath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } catch (error) { console.error('[get_codebase_summary] Error:', error); return { content: [{ type: 'text', text: `Error retrieving codebase summary: ${error}` }], isError: true }; } }); // 6. get_history_log tool server.tool('get_history_log', 'Retrieves the history log', {}, async () => { try { const content = await fs.readFile(historyLogPath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } catch (error) { console.error('[get_history_log] Error:', error); return { content: [{ type: 'text', text: `Error retrieving history log: ${error}` }], isError: true }; } }); // 7. get_feature_tasks tool server.tool('get_feature_tasks', 'Retrieves tasks for a specific feature', { feature_identifier: zod_1.z.string() }, async ({ feature_identifier }) => { try { const filePath = path.join(featureTasksDir, `feature_${feature_identifier}_tasks.md`); const content = await fs.readFile(filePath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } catch (error) { console.error('[get_feature_tasks] Error:', error); return { content: [{ type: 'text', text: `Error retrieving feature tasks: ${error}` }], isError: true }; } }); // --- Express App Setup --- const app = (0, express_1.default)(); app.use(express_1.default.json()); // Middleware to parse JSON bodies // --- Health Check Endpoint --- app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString(), version: SERVER_VERSION, name: SERVER_NAME }); }); // --- Transport Management --- // Store transports keyed by sessionId to handle multiple clients const transports = {}; // --- SSE Endpoint --- // Client connects here to establish the MCP connection app.get('/sse', async (req, res) => { console.log(`[${new Date().toISOString()}] Client connected via SSE`); // Use '/messages' as the endpoint for the client to POST messages back to const transport = new sse_js_1.SSEServerTransport('/messages', res); transports[transport.sessionId] = transport; // Handle client disconnection req.on('close', () => { console.log(`[${new Date().toISOString()}] Client disconnected (Session ID: ${transport.sessionId})`); delete transports[transport.sessionId]; transport.close(); // Ensure transport resources are cleaned up }); try { // Connect the McpServer to this specific client's transport await server.connect(transport); console.log(`[${new Date().toISOString()}] MCP Server connected to transport (Session ID: ${transport.sessionId})`); } catch (error) { console.error(`[${new Date().toISOString()}] Error connecting MCP Server to transport (Session ID: ${transport.sessionId}):`, error); // Ensure response ends if connection fails if (!res.writableEnded) { res.status(500).end(); } delete transports[transport.sessionId]; // Clean up failed connection } }); // --- Messages Endpoint --- // Client POSTs messages (like tool calls) to this endpoint app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId; const transport = transports[sessionId]; if (transport) { try { console.log(`[${new Date().toISOString()}] Received message for Session ID: ${sessionId}`); await transport.handlePostMessage(req, res); console.log(`[${new Date().toISOString()}] Handled message for Session ID: ${sessionId}`); } catch (error) { console.error(`[${new Date().toISOString()}] Error handling POST message for Session ID: ${sessionId}:`, error); if (!res.headersSent) { res.status(500).send('Internal Server Error'); } } } else { console.warn(`[${new Date().toISOString()}] No active transport found for Session ID: ${sessionId}`); res.status(400).send(`No active MCP session found for ID: ${sessionId}`); } }); // --- Server Start --- app.listen(PORT, async () => { console.log(`[${new Date().toISOString()}] ${SERVER_NAME} v${SERVER_VERSION} listening on port ${PORT}`); console.log(`[${new Date().toISOString()}] SSE endpoint available at http://localhost:${PORT}/sse`); console.log(`[${new Date().toISOString()}] Messages endpoint available at http://localhost:${PORT}/messages`); // Run initial codebase analysis if needed try { console.log('[STARTUP] Running initial codebase analysis...'); await codebaseAnalyzer.analyze(process.cwd()); await historyLogger.log(`${SERVER_NAME} v${SERVER_VERSION} started and ran initial codebase analysis`); console.log('[STARTUP] Codebase analysis completed. Check lamplighter_context/codebase_summary.md'); } catch (error) { console.error('[STARTUP] Error during codebase analysis:', error); } }); // Basic error handling for uncaught exceptions process.on('uncaughtException', (error) => { console.error(`[${new Date().toISOString()}] Uncaught Exception:`, error); // Optionally exit gracefully - consider PM2 or similar for production // process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error(`[${new Date().toISOString()}] Unhandled Rejection at:`, promise, 'reason:', reason); // Optionally exit gracefully // process.exit(1); });