Skip to main content
Glama

CodeAnalysis MCP Server

by 0xjcf
quality.ts21.2 kB
import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import figures from 'figures'; import fs from 'fs'; import path from 'path'; import { getClient, callTool, closeClient } from '../utils/mcp-client.js'; // Add this type at the top of the file type CountsType = { errors: number; warnings: number; info: number }; export function registerQualityCommands(program: Command) { const qualityCommand = program .command('quality') .description('Analyze code quality and best practices'); // Code quality analysis command qualityCommand .command('analyze <repository-path>') .description('Analyze code quality issues') .option('-i, --include <patterns...>', 'File patterns to include') .option('-e, --exclude <patterns...>', 'File patterns to exclude') .option('-m, --max-issues <number>', 'Maximum issues to report', parseInt) .option('-s, --min-severity <level>', 'Minimum severity level (error, warning, info)', 'warning') .option('-o, --output-file <file>', 'Save report to a file') .option('--html', 'Generate HTML report (requires output-file)') .action(async (repositoryPath, options, command) => { const { serverPath, debug, output } = command.parent.parent.opts(); try { const spinner = ora('Analyzing code quality...').start(); // Connect to server const client = await getClient(serverPath, debug); // Call the analyze-quality tool const result = await callTool('analyze-quality', { repositoryPath, includePaths: options.include, excludePaths: options.exclude, maxIssues: options.maxIssues, minSeverity: options.minSeverity }, debug); spinner.succeed('Quality analysis complete'); // Parse the result content let qualityReport; if (result && result.content && Array.isArray(result.content)) { for (const item of result.content) { if (item.type === 'text') { try { qualityReport = JSON.parse(item.text); break; } catch (e) { // Not JSON, use as is } } } } if (!qualityReport) { console.error(chalk.red('Failed to parse quality report')); process.exit(1); } // Calculate totals const totalIssues = qualityReport.issueCount.errors + qualityReport.issueCount.warnings + qualityReport.issueCount.info; // Display summary console.log(chalk.bold('\nQuality Report Summary:')); console.log(`Total issues found: ${chalk.bold(totalIssues)}`); console.log(`Errors: ${chalk.red(qualityReport.issueCount.errors)}`); console.log(`Warnings: ${chalk.yellow(qualityReport.issueCount.warnings)}`); console.log(`Info: ${chalk.blue(qualityReport.issueCount.info)}`); console.log(`Files analyzed: ${chalk.bold(qualityReport.metadata.analyzedFiles)}`); // Display top issues by rule console.log(chalk.bold('\nTop Issues by Rule:')); const ruleEntries = Object.entries(qualityReport.summary.byRule) .map(([rule, counts]) => ({ rule, total: (counts as CountsType).errors + (counts as CountsType).warnings + (counts as CountsType).info, counts: counts as CountsType })) .sort((a, b) => b.total - a.total) .slice(0, 5); for (const entry of ruleEntries) { console.log(`${chalk.bold(entry.rule)}: ${entry.total} issues`); } // Display top files with issues console.log(chalk.bold('\nTop Files with Issues:')); const fileEntries = Object.entries(qualityReport.summary.byFile) .map(([file, counts]) => ({ file, total: (counts as CountsType).errors + (counts as CountsType).warnings + (counts as CountsType).info, counts: counts as CountsType })) .sort((a, b) => b.total - a.total) .slice(0, 5); for (const entry of fileEntries) { console.log(`${chalk.bold(entry.file)}: ${entry.total} issues`); } // Display issues list (limited) if (qualityReport.issues.length > 0) { console.log(chalk.bold('\nIssues:')); const limitedIssues = qualityReport.issues.slice(0, 10); for (const issue of limitedIssues) { const severityColor = issue.severity === 'error' ? chalk.red : issue.severity === 'warning' ? chalk.yellow : chalk.blue; console.log(`${severityColor(issue.severity.toUpperCase())} ${issue.file}:${issue.line || '?'} - ${issue.message} [${issue.rule}]`); if (issue.context) { console.log(` ${chalk.gray(issue.context)}`); } } if (qualityReport.issues.length > 10) { console.log(chalk.gray(`\n...and ${qualityReport.issues.length - 10} more issues`)); } } // Save report to file if requested if (options.outputFile) { const outputPath = path.resolve(options.outputFile); if (options.html) { // Generate HTML report const htmlReport = generateHtmlReport(qualityReport); fs.writeFileSync(outputPath, htmlReport); } else { // Save JSON report fs.writeFileSync(outputPath, JSON.stringify(qualityReport, null, 2)); } console.log(chalk.green(`\nReport saved to: ${outputPath}`)); } } catch (error) { console.error(chalk.red(`${figures.cross} Quality analysis failed: ${(error as Error).message}`)); process.exit(1); } finally { await closeClient(); } }); // Quick summary command as suggested in the review qualityCommand .command('summary <repository-path>') .description('Generate a quick quality summary') .option('-s, --min-severity <level>', 'Minimum severity level (error, warning, info)', 'error') .action(async (repositoryPath, options, command) => { const { serverPath, debug } = command.parent.parent.opts(); try { const spinner = ora('Generating quick quality summary...').start(); // Connect to server const client = await getClient(serverPath, debug); // Call the analyze-quality tool with limited scope const result = await callTool('analyze-quality', { repositoryPath, maxIssues: 100, minSeverity: options.minSeverity }, debug); spinner.succeed('Quality summary complete'); // Parse the result content let qualityReport; if (result && result.content && Array.isArray(result.content)) { for (const item of result.content) { if (item.type === 'text') { try { qualityReport = JSON.parse(item.text); break; } catch (e) { // Not JSON, use as is } } } } if (!qualityReport) { console.error(chalk.red('Failed to parse quality report')); process.exit(1); } // Display compact summary console.log(chalk.bold('\nCode Quality Summary:')); console.log(`Files analyzed: ${chalk.bold(qualityReport.metadata.analyzedFiles)}`); console.log(`Issues: ${chalk.red(qualityReport.issueCount.errors)} errors, ${chalk.yellow(qualityReport.issueCount.warnings)} warnings`); // Most critical issues if (qualityReport.issues.length > 0) { console.log(chalk.bold('\nTop Issues:')); const criticalIssues = qualityReport.issues .filter((issue: {severity: string}) => issue.severity === 'error') .slice(0, 5); if (criticalIssues.length > 0) { for (const issue of criticalIssues) { console.log(`${chalk.red('ERROR')} ${issue.file}:${issue.line || '?'} - ${issue.message}`); } } else { console.log(chalk.green('No critical errors found!')); } } } catch (error) { console.error(chalk.red(`${figures.cross} Quality summary failed: ${(error as Error).message}`)); process.exit(1); } finally { await closeClient(); } }); // List quality rules command qualityCommand .command('rules') .description('List available quality rules') .action(async (options, command) => { const { serverPath, debug } = command.parent.parent.opts(); try { const spinner = ora('Fetching quality rules...').start(); // Connect to server const client = await getClient(serverPath, debug); // Call the list-quality-rules tool const result = await callTool('list-quality-rules', {}, debug); spinner.succeed('Quality rules retrieved'); // Parse the result content let rules; if (result && result.content && Array.isArray(result.content)) { for (const item of result.content) { if (item.type === 'text') { try { rules = JSON.parse(item.text); break; } catch (e) { // Not JSON, use as is } } } } if (!rules || !Array.isArray(rules)) { console.error(chalk.red('Failed to parse quality rules')); process.exit(1); } // Display rules console.log(chalk.bold('\nAvailable Quality Rules:')); console.log('-------------------------'); for (const rule of rules) { const severityColor = rule.severity === 'error' ? chalk.red : rule.severity === 'warning' ? chalk.yellow : chalk.blue; console.log(`\n${chalk.bold(rule.id)} [${severityColor(rule.severity)}]`); console.log(`${chalk.dim('Name:')} ${rule.name}`); console.log(`${chalk.dim('Description:')} ${rule.description}`); console.log(`${chalk.dim('Languages:')} ${rule.languages.join(', ')}`); } } catch (error) { console.error(chalk.red(`${figures.cross} Failed to retrieve quality rules: ${(error as Error).message}`)); process.exit(1); } finally { await closeClient(); } }); return qualityCommand; } /** * Generate an HTML report from quality data */ function generateHtmlReport(data: any): string { // Implementation for HTML report generation let html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Code Quality Report</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; color: #333; } h1, h2, h3 { color: #2c3e50; } .summary { display: flex; gap: 20px; margin-bottom: 20px; } .summary-box { padding: 15px; border-radius: 5px; flex: 1; } .errors { background-color: #ffecec; border-left: 5px solid #f44336; } .warnings { background-color: #fffaec; border-left: 5px solid #ff9800; } .info { background-color: #ecf6ff; border-left: 5px solid #2196f3; } .tabs { display: flex; margin: 20px 0; border-bottom: 1px solid #ddd; } .tab { padding: 10px 20px; cursor: pointer; } .tab.active { border-bottom: 3px solid #3f51b5; color: #3f51b5; font-weight: bold; } .tab-content { display: none; } .tab-content.active { display: block; } .filters { margin: 20px 0; padding: 15px; background-color: #f5f5f5; border-radius: 5px; } .filters select, .filters input { padding: 8px; margin-right: 10px; } .issues-table { width: 100%; border-collapse: collapse; margin-top: 20px; } .issues-table th, .issues-table td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; } .issues-table th { background-color: #f2f2f2; } .severity-error { color: #f44336; font-weight: bold; } .severity-warning { color: #ff9800; font-weight: bold; } .severity-info { color: #2196f3; font-weight: bold; } .context { background-color: #f9f9f9; padding: 5px; margin-top: 5px; border-radius: 3px; font-family: monospace; } .file-link { color: #3f51b5; text-decoration: none; } .file-link:hover { text-decoration: underline; } .chart-container { height: 300px; margin: 20px 0; } </style> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <h1>Code Quality Report</h1> <div class="summary"> <div class="summary-box errors"> <h3>Errors</h3> <p>${data.issueCount.errors}</p> </div> <div class="summary-box warnings"> <h3>Warnings</h3> <p>${data.issueCount.warnings}</p> </div> <div class="summary-box info"> <h3>Info</h3> <p>${data.issueCount.info}</p> </div> </div> <div class="tabs"> <div class="tab active" data-tab="issues">Issues</div> <div class="tab" data-tab="files">Files</div> <div class="tab" data-tab="rules">Rules</div> <div class="tab" data-tab="charts">Charts</div> </div> <div class="tab-content active" id="issues-tab"> <div class="filters"> <label for="severity-filter">Severity:</label> <select id="severity-filter"> <option value="all">All</option> <option value="error">Errors only</option> <option value="warning">Warnings and errors</option> </select> <label for="issue-search">Search:</label> <input type="text" id="issue-search" placeholder="Filter issues..."> </div> <table class="issues-table" id="issues-table"> <thead> <tr> <th>Severity</th> <th>File</th> <th>Line</th> <th>Rule</th> <th>Message</th> </tr> </thead> <tbody> `; for (const issue of data.issues) { const severityClass = `severity-${issue.severity}`; html += ` <tr data-severity="${issue.severity}"> <td class="${severityClass}">${issue.severity.toUpperCase()}</td> <td><a href="#" class="file-link">${issue.file}</a></td> <td>${issue.line || ''}</td> <td>${issue.rule}</td> <td>${issue.message}${issue.context ? `<div class="context">${issue.context}</div>` : ''}</td> </tr> `; } html += ` </tbody> </table> </div> <div class="tab-content" id="files-tab"> <table class="issues-table"> <thead> <tr> <th>File</th> <th>Total Issues</th> <th>Errors</th> <th>Warnings</th> <th>Info</th> </tr> </thead> <tbody> `; const fileEntries = Object.entries(data.summary.byFile) .map(([file, counts]) => ({ file, total: (counts as CountsType).errors + (counts as CountsType).warnings + (counts as CountsType).info, counts: counts as CountsType })) .sort((a, b) => b.total - a.total); for (const entry of fileEntries) { html += ` <tr> <td><a href="#" class="file-link">${entry.file}</a></td> <td>${entry.total}</td> <td class="severity-error">${entry.counts.errors}</td> <td class="severity-warning">${entry.counts.warnings}</td> <td class="severity-info">${entry.counts.info}</td> </tr> `; } html += ` </tbody> </table> </div> <div class="tab-content" id="rules-tab"> <table class="issues-table"> <thead> <tr> <th>Rule</th> <th>Total Issues</th> <th>Errors</th> <th>Warnings</th> <th>Info</th> </tr> </thead> <tbody> `; const ruleEntries = Object.entries(data.summary.byRule) .map(([rule, counts]) => ({ rule, total: (counts as CountsType).errors + (counts as CountsType).warnings + (counts as CountsType).info, counts: counts as CountsType })) .sort((a, b) => b.total - a.total); for (const entry of ruleEntries) { html += ` <tr> <td>${entry.rule}</td> <td>${entry.total}</td> <td class="severity-error">${entry.counts.errors}</td> <td class="severity-warning">${entry.counts.warnings}</td> <td class="severity-info">${entry.counts.info}</td> </tr> `; } html += ` </tbody> </table> </div> <div class="tab-content" id="charts-tab"> <h2>Issues by Severity</h2> <div class="chart-container"> <canvas id="severity-chart"></canvas> </div> <h2>Top 5 Rules</h2> <div class="chart-container"> <canvas id="rules-chart"></canvas> </div> <h2>Top 5 Files</h2> <div class="chart-container"> <canvas id="files-chart"></canvas> </div> </div> <script> // Tab navigation const tabs = document.querySelectorAll('.tab'); const tabContents = document.querySelectorAll('.tab-content'); tabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.getAttribute('data-tab'); // Update active tab tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); // Show active content tabContents.forEach(content => { content.classList.remove('active'); if (content.id === tabId + '-tab') { content.classList.add('active'); } }); }); }); // Issue filtering const severityFilter = document.getElementById('severity-filter'); const issueSearch = document.getElementById('issue-search'); const issuesTable = document.getElementById('issues-table'); function filterIssues() { const severity = severityFilter.value; const searchText = issueSearch.value.toLowerCase(); const rows = issuesTable.querySelectorAll('tbody tr'); rows.forEach(row => { const rowSeverity = row.getAttribute('data-severity'); const rowText = row.textContent.toLowerCase(); let showBySeverity = true; if (severity === 'error') { showBySeverity = rowSeverity === 'error'; } else if (severity === 'warning') { showBySeverity = rowSeverity === 'error' || rowSeverity === 'warning'; } const showBySearch = searchText === '' || rowText.includes(searchText); row.style.display = showBySeverity && showBySearch ? '' : 'none'; }); } severityFilter.addEventListener('change', filterIssues); issueSearch.addEventListener('input', filterIssues); // Charts window.addEventListener('load', () => { // Severity chart const severityData = [ ${data.issueCount.errors}, ${data.issueCount.warnings}, ${data.issueCount.info} ]; new Chart(document.getElementById('severity-chart'), { type: 'pie', data: { labels: ['Errors', 'Warnings', 'Info'], datasets: [{ data: severityData, backgroundColor: ['#f44336', '#ff9800', '#2196f3'] }] }, options: { responsive: true, maintainAspectRatio: false } }); // Top rules chart const topRules = ${JSON.stringify(ruleEntries.slice(0, 5).map(entry => ({ rule: entry.rule, count: entry.total })))}; new Chart(document.getElementById('rules-chart'), { type: 'bar', data: { labels: topRules.map(r => r.rule), datasets: [{ label: 'Issues', data: topRules.map(r => r.count), backgroundColor: '#3f51b5' }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); // Top files chart const topFiles = ${JSON.stringify(fileEntries.slice(0, 5).map(entry => ({ file: entry.file, count: entry.total })))}; new Chart(document.getElementById('files-chart'), { type: 'bar', data: { labels: topFiles.map(f => f.file), datasets: [{ label: 'Issues', data: topFiles.map(f => f.count), backgroundColor: '#4caf50' }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); }); </script> </body> </html> `; return html; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/0xjcf/MCP_CodeAnalysis'

If you have feedback or need assistance with the MCP directory API, please join our Discord server