UNPKG

code-auditor-mcp

Version:

Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more

484 lines (472 loc) • 22.3 kB
#!/usr/bin/env node /** * MCP-UI HTTP Server Extension for Code Auditor * * This extends the existing MCP functionality with HTTP/UI capabilities * while maintaining the original stdio MCP server functionality. */ // Import existing MCP infrastructure import express from 'express'; import cors from 'cors'; import { randomUUID } from 'crypto'; // Note: StreamableHTTPServerTransport may not be available yet - using Server with Express instead import { createUIResource } from '@mcp-ui/server'; // Import shared tool functionality import { tools, uiTools, ToolHandlers } from './mcp-tools-shared.js'; import chalk from 'chalk'; const app = express(); const PORT = process.env.MCP_UI_PORT || 3001; // Session management - stores active Server instances const sessions = new Map(); // Essential middleware app.use(express.json()); // Critical CORS configuration for MCP session management app.use(cors({ origin: '*', // Configure appropriately for production exposedHeaders: ['Mcp-Session-Id'], // Allow client to read session ID allowedHeaders: ['Content-Type', 'mcp-session-id'], // Allow session ID in requests })); /** * Session handler utility for GET and DELETE endpoints */ function getSessionHandler(req, res) { const sessionId = req.headers['mcp-session-id']; if (!sessionId) { return res.status(400).json({ error: 'Missing mcp-session-id header' }); } const transport = transports.get(sessionId); if (!transport) { return res.status(404).json({ error: 'Session not found' }); } return transport; } /** * Register all audit tools with UI capabilities */ async function registerAllAuditTools(server) { // Register all standard tools for (const tool of tools) { server.setRequestHandler({ method: 'tools/call', params: { name: tool.name } }, async (request) => { const args = request.params.arguments || {}; // Route to appropriate handler switch (tool.name) { case 'audit': return { content: [{ type: 'text', text: JSON.stringify(await ToolHandlers.handleAudit(args), null, 2) }] }; case 'audit_health': return { content: [{ type: 'text', text: JSON.stringify(await ToolHandlers.handleAuditHealth(args), null, 2) }] }; case 'search_code': return { content: [{ type: 'text', text: JSON.stringify(await ToolHandlers.handleSearchCode(args), null, 2) }] }; case 'find_definition': return { content: [{ type: 'text', text: JSON.stringify(await ToolHandlers.handleFindDefinition(args), null, 2) }] }; // Add other handlers as needed... default: throw new Error(`Handler not implemented for tool: ${tool.name}`); } }); } // Register UI-specific tools for (const tool of uiTools) { server.setRequestHandler({ method: 'tools/call', params: { name: tool.name } }, async (request) => { const args = request.params.arguments || {}; switch (tool.name) { case 'audit_dashboard': return await handleAuditDashboard(args); case 'code_map_viewer': return await handleCodeMapViewer(args); default: throw new Error(`UI handler not implemented for tool: ${tool.name}`); } }); } // Register tools list handler server.setRequestHandler({ method: 'tools/list' }, async () => { const allTools = [...tools, ...uiTools].map(tool => ({ name: tool.name, description: tool.description, inputSchema: { type: 'object', properties: tool.parameters.reduce((acc, param) => { acc[param.name] = { type: param.type, description: param.description, ...(param.default !== undefined && { default: param.default }), ...(param.enum && { enum: param.enum }), }; return acc; }, {}), required: tool.parameters.filter(p => p.required).map(p => p.name), }, })); return { tools: allTools }; }); } /** * Handle audit dashboard UI tool */ async function handleAuditDashboard(args) { try { // Run the audit using shared handler const auditResult = await ToolHandlers.handleAudit(args); // Create session-specific data storage key const sessionKey = randomUUID(); // Store audit results for dashboard access global.auditSessions = global.auditSessions || new Map(); global.auditSessions.set(sessionKey, { auditResult, timestamp: new Date().toISOString(), path: args.path || '.' }); // Generate UI resource pointing to dashboard const uiResource = createUIResource({ uri: `ui://code-auditor/dashboard/${sessionKey}`, content: { type: 'externalUrl', iframeUrl: `http://localhost:${PORT}/dashboard/${sessionKey}` }, encoding: 'text' }); return { content: [uiResource] }; } catch (error) { throw new Error(`Audit failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Handle code map viewer UI tool */ async function handleCodeMapViewer(args) { try { // Generate audit result to get code map const auditResult = await ToolHandlers.handleAudit({ ...args, generateCodeMap: true, indexFunctions: true }); const sessionKey = randomUUID(); global.codeMapSessions = global.codeMapSessions || new Map(); global.codeMapSessions.set(sessionKey, { codeMap: auditResult.codeMap, timestamp: new Date().toISOString(), path: args.path || '.' }); const uiResource = createUIResource({ uri: `ui://code-auditor/codemap/${sessionKey}`, content: { type: 'externalUrl', iframeUrl: `http://localhost:${PORT}/codemap/${sessionKey}` }, encoding: 'text' }); return { content: [uiResource] }; } catch (error) { throw new Error(`Code map generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Main MCP endpoint - handles POST, GET, and DELETE */ app.all('/mcp', async (req, res) => { try { if (req.method === 'POST') { // Handle JSON-RPC requests (initialization and tool calls) const sessionId = req.headers['mcp-session-id']; if (sessionId && transports.has(sessionId)) { // Continue existing session const transport = transports.get(sessionId); await transport.handleRequest(req.body, res); } else { // Check for new session initialization if (isInitializeRequest(req.body)) { // Create new transport with session management const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId, transport) => { transports.set(sessionId, transport); console.error(chalk.blue('[MCP-UI]'), `Session initialized: ${sessionId}`); }, onclose: (sessionId) => { transports.delete(sessionId); console.error(chalk.blue('[MCP-UI]'), `Session closed: ${sessionId}`); } }); // Create new MCP server instance for this session const server = new McpServer({ name: 'code-auditor-ui', version: '1.0.0' }, { capabilities: { tools: {} } }); // Register all audit tools with UI capabilities await registerAllAuditTools(server); // Connect server to transport await server.connect(transport); // Handle the initialization request await transport.handleRequest(req.body, res); } else { res.status(400).json({ error: 'Invalid request - missing session or not an initialization' }); } } } else if (req.method === 'GET') { // Handle Server-Sent Events stream for real-time updates const transport = getSessionHandler(req, res); if (transport) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); await transport.handleRequest(null, res); } } else if (req.method === 'DELETE') { // Handle session termination const transport = getSessionHandler(req, res); if (transport) { transport.close(); res.status(204).end(); } } else { res.status(405).json({ error: 'Method not allowed' }); } } catch (error) { console.error(chalk.red('[MCP-UI ERROR]'), 'Request handling failed:', error); res.status(500).json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * Dashboard route - serves the interactive audit dashboard */ app.get('/dashboard/:sessionKey', (req, res) => { const { sessionKey } = req.params; const sessionData = global.auditSessions?.get(sessionKey); if (!sessionData) { return res.status(404).send('Audit session not found'); } const { auditResult } = sessionData; // Enhanced dashboard HTML with better styling and interactivity const dashboardHtml = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Code Audit Dashboard</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8fafc; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 30px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); } .header h1 { font-size: 2.5rem; margin-bottom: 10px; } .header p { font-size: 1.1rem; opacity: 0.9; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); border-left: 4px solid #667eea; transition: transform 0.2s; } .stat-card:hover { transform: translateY(-2px); } .stat-card h3 { color: #4a5568; margin-bottom: 15px; font-size: 1.1rem; } .stat-value { font-size: 2rem; font-weight: bold; color: #2d3748; margin-bottom: 5px; } .stat-label { color: #718096; font-size: 0.9rem; } .severity-critical { border-left-color: #e53e3e; } .severity-warning { border-left-color: #dd6b20; } .severity-info { border-left-color: #3182ce; } .violations-section { background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); overflow: hidden; } .section-header { background: #f7fafc; padding: 20px; border-bottom: 1px solid #e2e8f0; } .section-header h2 { color: #2d3748; font-size: 1.5rem; } .filters { display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap; } .filter-btn { padding: 8px 16px; border: 1px solid #e2e8f0; background: white; border-radius: 6px; cursor: pointer; transition: all 0.2s; } .filter-btn:hover, .filter-btn.active { background: #667eea; color: white; border-color: #667eea; } .violations-list { max-height: 600px; overflow-y: auto; } .violation { padding: 20px; border-bottom: 1px solid #f1f5f9; transition: background 0.2s; } .violation:hover { background: #f8fafc; } .violation:last-child { border-bottom: none; } .violation-header { display: flex; justify-content: between; align-items: start; margin-bottom: 10px; } .violation-title { font-weight: 600; color: #2d3748; font-size: 1.1rem; margin-bottom: 5px; } .violation-meta { display: flex; gap: 15px; font-size: 0.9rem; color: #718096; margin-bottom: 10px; } .violation-file { font-family: 'Monaco', 'Menlo', monospace; background: #f7fafc; padding: 4px 8px; border-radius: 4px; } .severity-badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; } .severity-critical { background: #fed7d7; color: #c53030; } .severity-warning { background: #feebc8; color: #c05621; } .severity-info { background: #bee3f8; color: #2c5aa0; } .recommendation { background: #f0fff4; border: 1px solid #9ae6b4; border-radius: 6px; padding: 12px; margin-top: 10px; } .recommendation::before { content: "šŸ’” "; font-size: 1.2rem; } .health-score { text-align: center; padding: 20px; } .health-circle { width: 120px; height: 120px; border-radius: 50%; margin: 0 auto 15px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; color: white; } .loading { text-align: center; padding: 40px; color: #718096; } </style> </head> <body> <div class="container"> <div class="header"> <h1>šŸ” Code Audit Dashboard</h1> <p>Interactive analysis results for ${sessionData.path} • ${auditResult.summary?.filesAnalyzed || 0} files analyzed</p> </div> <div class="stats-grid"> <div class="stat-card"> <h3>šŸ“Š Health Score</h3> <div class="health-score"> <div class="health-circle" style="background: ${(auditResult.summary?.healthScore || 0) >= 80 ? '#48bb78' : (auditResult.summary?.healthScore || 0) >= 60 ? '#ed8936' : '#f56565'}"> ${auditResult.summary?.healthScore || 0}% </div> <div class="stat-label">Overall code quality</div> </div> </div> <div class="stat-card severity-critical"> <h3>🚨 Critical Issues</h3> <div class="stat-value">${auditResult.summary?.criticalIssues || 0}</div> <div class="stat-label">Requires immediate attention</div> </div> <div class="stat-card severity-warning"> <h3>āš ļø Warnings</h3> <div class="stat-value">${auditResult.summary?.warnings || 0}</div> <div class="stat-label">Should be addressed</div> </div> <div class="stat-card severity-info"> <h3>šŸ’” Suggestions</h3> <div class="stat-value">${auditResult.summary?.suggestions || 0}</div> <div class="stat-label">Improvement opportunities</div> </div> </div> <div class="violations-section"> <div class="section-header"> <h2>🚨 Violations</h2> <div class="filters"> <button class="filter-btn active" onclick="filterViolations('all')">All</button> <button class="filter-btn" onclick="filterViolations('critical')">Critical</button> <button class="filter-btn" onclick="filterViolations('warning')">Warnings</button> <button class="filter-btn" onclick="filterViolations('info')">Info</button> </div> </div> <div class="violations-list" id="violations-list"> ${generateViolationsHTML(auditResult)} </div> </div> </div> <script> let allViolations = ${JSON.stringify(ToolHandlers.getAllViolations(auditResult) || [])}; function filterViolations(severity) { const buttons = document.querySelectorAll('.filter-btn'); buttons.forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); const violationsList = document.getElementById('violations-list'); let filteredViolations = severity === 'all' ? allViolations : allViolations.filter(v => v.severity === severity); violationsList.innerHTML = filteredViolations.length === 0 ? '<div class="loading">No violations found for this filter.</div>' : filteredViolations.map(violation => createViolationHTML(violation)).join(''); } function createViolationHTML(violation) { return \` <div class="violation" data-severity="\${violation.severity}"> <div class="violation-title">\${violation.message}</div> <div class="violation-meta"> <span class="violation-file">\${violation.file}:\${violation.line}:\${violation.column}</span> <span class="severity-badge severity-\${violation.severity}">\${violation.severity}</span> <span>Analyzer: \${violation.analyzer}</span> </div> \${violation.recommendation ? \`<div class="recommendation">\${violation.recommendation}</div>\` : ''} </div> \`; } // Initialize dashboard console.log('šŸŽÆ Interactive Audit Dashboard Loaded'); console.log('šŸ“Š Audit Data:', { totalViolations: ${auditResult.summary?.totalViolations || 0}, healthScore: ${auditResult.summary?.healthScore || 0}, filesAnalyzed: ${auditResult.summary?.filesAnalyzed || 0} }); </script> </body> </html> `; res.send(dashboardHtml); }); // Helper function to generate violations HTML function generateViolationsHTML(auditResult) { const violations = ToolHandlers.getAllViolations(auditResult).slice(0, 50); // Limit for performance if (violations.length === 0) { return '<div class="loading">No violations found! šŸŽ‰</div>'; } return violations.map(violation => ` <div class="violation" data-severity="${violation.severity}"> <div class="violation-title">${violation.message}</div> <div class="violation-meta"> <span class="violation-file">${violation.file}:${violation.line}:${violation.column}</span> <span class="severity-badge severity-${violation.severity}">${violation.severity}</span> <span>Analyzer: ${violation.analyzer}</span> </div> ${violation.recommendation ? `<div class="recommendation">${violation.recommendation}</div>` : ''} </div> `).join(''); } /** * Health check endpoint */ app.get('/health', (req, res) => { res.json({ status: 'healthy', activeSessions: transports.size, timestamp: new Date().toISOString(), version: '1.0.0' }); }); /** * API endpoint to get audit data as JSON */ app.get('/api/audit/:sessionKey', (req, res) => { const { sessionKey } = req.params; const sessionData = global.auditSessions?.get(sessionKey); if (!sessionData) { return res.status(404).json({ error: 'Audit session not found' }); } res.json(sessionData); }); /** * Start the MCP-UI HTTP server */ function startMcpUIServer() { app.listen(PORT, () => { console.error(chalk.green('šŸš€ MCP-UI Code Auditor Server running on'), chalk.cyan(`http://localhost:${PORT}`)); console.error(chalk.blue('šŸ“” MCP endpoint:'), chalk.cyan(`http://localhost:${PORT}/mcp`)); console.error(chalk.blue('ā¤ļø Health check:'), chalk.cyan(`http://localhost:${PORT}/health`)); console.error(chalk.gray('Ready to accept interactive audit requests...')); }); // Graceful shutdown process.on('SIGINT', () => { console.error(chalk.yellow('\nšŸ›‘ Shutting down MCP-UI server...')); // Close all active sessions for (const [sessionId, transport] of transports) { try { transport.close(); console.error(chalk.blue('[MCP-UI]'), `Closed session: ${sessionId}`); } catch (error) { console.error(chalk.red('[MCP-UI ERROR]'), `Error closing session ${sessionId}:`, error); } } process.exit(0); }); } // Start the server if this file is run directly if (import.meta.url === `file://${process.argv[1]}`) { startMcpUIServer(); } export { startMcpUIServer, app }; //# sourceMappingURL=mcp-ui-server.js.map