bugger-mcp
Version:
MCP Server for managing bugs, feature requests, and improvements
405 lines (339 loc) • 13.5 kB
text/typescript
import chalk from 'chalk';
// Disable colors in MCP environment since they show as codes
chalk.level = 0;
// Color and formatting utilities for MCP output - simplified without colors
const colors = {
// Status colors - no colors, just return text
red: (text: string) => text,
green: (text: string) => text,
yellow: (text: string) => text,
blue: (text: string) => text,
orange: (text: string) => text,
purple: (text: string) => text,
// Priority colors - no colors, just return text
critical: (text: string) => text,
high: (text: string) => text,
medium: (text: string) => text,
low: (text: string) => text,
// General formatting - no colors, just return text
highlight: (text: string) => text,
success: (text: string) => text,
info: (text: string) => text,
warning: (text: string) => text,
error: (text: string) => text
};
// Unified output formatting interface
interface OutputOptions {
includeHeaders?: boolean;
maxContentLength?: number;
showMetadata?: boolean;
showTokenUsage?: boolean;
}
interface TableColumn {
key: string;
header: string;
width?: number;
align?: 'left' | 'center' | 'right';
formatter?: (value: any) => string;
}
/**
* Unified table formatter for consistent output across all tools
*/
export function formatTable(data: any[], columns: TableColumn[], options: OutputOptions = {}): string {
if (data.length === 0) {
return "No items found.";
}
const { includeHeaders = true, maxContentLength = 50 } = options;
// Calculate column widths
const columnWidths = columns.map(col => {
const headerWidth = col.header.length;
const contentWidth = Math.max(...data.map(item => {
const value = col.formatter ? col.formatter(item[col.key]) : String(item[col.key] || '');
return Math.min(value.length, maxContentLength);
}));
return Math.max(headerWidth, contentWidth, col.width || 0);
});
let output = '';
// Add headers
if (includeHeaders) {
const headerRow = columns.map((col, i) => {
const padding = columnWidths[i] - col.header.length;
return col.header + ' '.repeat(Math.max(0, padding));
}).join(' | ');
const separator = columns.map((_, i) => '-'.repeat(columnWidths[i])).join('--|--');
output += headerRow + '\n';
output += separator + '\n';
}
// Add data rows
data.forEach(item => {
const row = columns.map((col, i) => {
let value = col.formatter ? col.formatter(item[col.key]) : String(item[col.key] || '');
// Truncate long content
if (value.length > maxContentLength) {
value = value.substring(0, maxContentLength - 3) + '...';
}
const padding = columnWidths[i] - value.length;
const align = col.align || 'left';
switch (align) {
case 'right':
return ' '.repeat(Math.max(0, padding)) + value;
case 'center':
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return ' '.repeat(leftPad) + value + ' '.repeat(rightPad);
default: // left
return value + ' '.repeat(Math.max(0, padding));
}
}).join(' | ');
output += row + '\n';
});
return output;
}
/**
* Format error message consistently
*/
export function formatError(message: string, context?: string): string {
let output = colors.error(`Error: ${message}`);
if (context) {
output += `\n${colors.info(`Context: ${context}`)}`;
}
return output;
}
/**
* Format success message consistently
*/
export function formatSuccess(message: string, details?: string): string {
let output = colors.success(message);
if (details) {
output += `\n${colors.info(details)}`;
}
return output;
}
/**
* Format metadata consistently
*/
export function formatMetadata(metadata: any): string {
let output = '';
if (metadata.total !== undefined) {
output += `Total: ${metadata.total}`;
if (metadata.showing !== undefined && metadata.showing !== metadata.total) {
output += ` (showing ${metadata.showing})`;
}
output += '\n';
}
if (metadata.offset !== undefined && metadata.offset > 0) {
output += `Results ${metadata.offset + 1}-${metadata.offset + (metadata.showing || metadata.total)}\n`;
}
return output;
}
/**
* Format token usage consistently
*/
export function formatTokenUsage(usage: { total: number; input: number; output: number }): string {
return `\nToken usage: ${usage.total} tokens (${usage.input} input, ${usage.output} output)`;
}
/**
* Get priority formatter
*/
export function getPriorityFormatter(): (priority: string) => string {
return (priority: string) => {
switch (priority?.toLowerCase()) {
case 'critical': return colors.critical(priority);
case 'high': return colors.high(priority);
case 'medium': return colors.medium(priority);
case 'low': return colors.low(priority);
default: return priority || '';
}
};
}
/**
* Get status formatter with colors
*/
export function getStatusFormatter(): (status: string) => string {
return (status: string) => {
const statusLower = status?.toLowerCase() || '';
if (statusLower.includes('open') || statusLower === 'proposed') return chalk.red(status);
else if (statusLower.includes('progress') || statusLower.includes('development')) return chalk.yellow(status);
else if (statusLower.includes('fixed') || statusLower.includes('completed') || statusLower === 'done') return chalk.green(status);
else if (statusLower.includes('closed') || statusLower === 'rejected') return chalk.gray(status);
else if (statusLower.includes('blocked')) return chalk.red.bold(status);
else if (statusLower.includes('discussion') || statusLower.includes('research')) return chalk.blue(status);
else if (statusLower.includes('approved')) return chalk.green.bold(status);
return status;
};
}
interface Bug {
id: string;
status: 'Open' | 'In Progress' | 'Fixed' | 'Closed' | 'Temporarily Resolved';
priority: 'Low' | 'Medium' | 'High' | 'Critical';
dateReported: string;
component: string;
title: string;
description: string;
expectedBehavior: string;
actualBehavior: string;
potentialRootCause?: string;
filesLikelyInvolved?: string[];
stepsToReproduce?: string[];
verification?: string[];
humanVerified?: boolean;
}
// FeatureRequest removed
interface Improvement {
id: string;
status: 'Proposed' | 'In Discussion' | 'Approved' | 'In Development' | 'Completed (Awaiting Human Verification)' | 'Completed' | 'Rejected';
priority: 'Low' | 'Medium' | 'High';
dateRequested: string;
dateCompleted?: string;
category: string;
requestedBy?: string;
title: string;
description: string;
currentState: string;
desiredState: string;
acceptanceCriteria: string[];
implementationDetails?: string;
potentialImplementation?: string;
filesLikelyInvolved?: string[];
dependencies?: string[];
effortEstimate?: 'Small' | 'Medium' | 'Large';
benefits?: string[];
}
export function formatBugs(bugs: Bug[]): string {
const columns: TableColumn[] = [
{ key: 'id', header: 'ID', width: 8 },
{ key: 'status', header: 'Status', formatter: getStatusFormatter() },
{ key: 'priority', header: 'Priority', formatter: getPriorityFormatter() },
{ key: 'component', header: 'Component' },
{ key: 'dateReported', header: 'Date', width: 10 },
{ key: 'title', header: 'Title' }
];
return formatTable(bugs, columns);
}
// formatFeatureRequests removed
export function formatImprovements(improvements: Improvement[]): string {
const columns: TableColumn[] = [
{ key: 'id', header: 'ID', width: 8 },
{ key: 'status', header: 'Status', formatter: getStatusFormatter() },
{ key: 'priority', header: 'Priority', formatter: getPriorityFormatter() },
{ key: 'category', header: 'Category' },
{ key: 'dateRequested', header: 'Date', width: 10 },
{ key: 'title', header: 'Title' }
];
return formatTable(improvements, columns);
}
export function formatImprovementsWithContext(improvements: any[]): string {
if (improvements.length === 0) {
return "No improvements found.";
}
let output = formatImprovements(improvements);
// Add code context for each improvement
improvements.forEach(improvement => {
if (improvement.codeContext && improvement.codeContext.length > 0) {
output += `\n${colors.highlight(`Code Context for ${improvement.id} - ${improvement.title}`)}\n`;
output += `${colors.info('Description:')} ${improvement.description}\n`;
output += `${colors.info('Current State:')} ${improvement.currentState}\n`;
output += `${colors.info('Desired State:')} ${improvement.desiredState}\n\n`;
improvement.codeContext.forEach((context: any) => {
output += `${colors.highlight(`File: ${context.file}`)}\n`;
if (context.error) {
output += `${colors.error(context.error)}\n\n`;
} else {
// Show token-optimized content display
const contentLength = context.content?.length || 0;
const estimatedTokens = Math.ceil(contentLength / 4);
output += `${colors.info(`Content (${contentLength} chars, ~${estimatedTokens} tokens)`)}\n`;
output += '```\n';
output += context.content;
output += '\n```\n\n';
}
});
}
});
return output;
}
export function formatSearchResults(results: any[], metadata?: any): string {
if (results.length === 0) {
return "No items found.";
}
let output = '';
// Add metadata header if provided
if (metadata) {
output += `Search Results (${metadata.showing} of ${metadata.total} total)\n`;
if (metadata.offset > 0) {
output += `Showing results ${metadata.offset + 1}-${metadata.offset + metadata.showing}\n`;
}
output += '\n';
}
const formattedResults = results.map(item => {
switch (item.type) {
case 'bug':
return formatBugs([item]);
// case 'feature': removed
case 'improvement':
return formatImprovements([item]);
default:
return JSON.stringify(item, null, 2);
}
}).join('\n\n');
return output + formattedResults;
}
export function formatBulkUpdateResults(results: any[], type: 'bugs' | 'improvements'): string {
const successCount = results.filter(r => r.status === 'success').length;
const errorCount = results.filter(r => r.status === 'error').length;
let output = '';
if (successCount > 0) {
const successfulResults = results.filter(r => r.status === 'success');
output += `${successfulResults.length} ${type} updated successfully:\n\n`;
// Create table-like format
const longestId = Math.max(...successfulResults.map(r => r.bugId?.length || r.featureId?.length || r.improvementId?.length || 0));
const longestStatus = Math.max(...successfulResults.map(r => r.message?.replace('Updated to ', '').length || 0));
successfulResults.forEach(r => {
const itemId = r.bugId || r.featureId || r.improvementId || '';
const status = r.message?.replace('Updated to ', '') || '';
const date = r.dateCompleted || new Date().toISOString().split('T')[0];
output += `${itemId.padEnd(longestId)} | ${status.padEnd(longestStatus)} | ${date}\n`;
});
if (errorCount > 0) {
output += '\n';
}
}
if (errorCount > 0) {
output += `${errorCount} ${type} failed to update:\n\n`;
results.filter(r => r.status === 'error').forEach(r => {
const itemId = r.bugId || r.featureId || r.improvementId || '';
output += `${itemId}: ${r.message}\n`;
});
}
return output;
}
export function formatStatistics(stats: any): string {
let output = 'Project Statistics\n\n';
if (stats.bugs) {
output += `Bugs (${stats.bugs.total} total)\n`;
output += formatStatusAndPriority(stats.bugs);
output += '\n';
}
// Features removed
if (stats.improvements) {
output += `Improvements (${stats.improvements.total} total)\n`;
output += formatStatusAndPriority(stats.improvements);
}
return output;
}
function formatStatusAndPriority(category: any): string {
let output = '';
if (category.byStatus) {
output += 'By Status:\n';
for (const [status, count] of Object.entries(category.byStatus)) {
output += ` ${status}: ${count}\n`;
}
}
if (category.byPriority) {
output += 'By Priority:\n';
for (const [priority, count] of Object.entries(category.byPriority)) {
output += ` ${priority}: ${count}\n`;
}
}
return output;
}