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
JavaScript
#!/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 = `
<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