lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
328 lines (327 loc) • 13.7 kB
JavaScript
;
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);
});