bugger-mcp
Version:
MCP Server for managing bugs, feature requests, and improvements
336 lines • 13.7 kB
JavaScript
// Bug management operations
import { formatBugs, formatBulkUpdateResults, formatTokenUsage } from './format.js';
import { TokenUsageTracker } from './token-usage-tracker.js';
export class BugManager {
tokenTracker;
constructor() {
this.tokenTracker = TokenUsageTracker.getInstance();
}
/**
* Create a new bug
*/
async createBug(db, args) {
const startTime = Date.now();
this.tokenTracker.startOperation('create_bug');
return this.createBugWithRetry(db, args, 0);
}
/**
* Create bug with retry mechanism to handle race conditions
*/
async createBugWithRetry(db, args, retryCount) {
const maxRetries = 3;
try {
return await this.createBugAttempt(db, args);
}
catch (error) {
if (retryCount < maxRetries && error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
// Wait a bit and retry with a new ID
await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 50));
return this.createBugWithRetry(db, args, retryCount + 1);
}
throw error;
}
}
/**
* Single attempt to create a bug
*/
async createBugAttempt(db, args) {
return new Promise(async (resolve, reject) => {
try {
// Generate ID
const bugId = args.bugId || await this.generateNextId(db, 'bug');
const bug = {
id: bugId,
status: 'Open',
priority: args.priority || 'Medium',
dateReported: new Date().toISOString().split('T')[0],
component: args.component || 'General',
title: args.title,
description: args.description,
expectedBehavior: args.expectedBehavior,
actualBehavior: args.actualBehavior,
potentialRootCause: args.potentialRootCause,
filesLikelyInvolved: args.filesLikelyInvolved || [],
stepsToReproduce: args.stepsToReproduce || [],
verification: [],
humanVerified: false
};
const insertQuery = `
INSERT INTO bugs (
id, status, priority, dateReported, component, title, description,
expectedBehavior, actualBehavior, potentialRootCause, filesLikelyInvolved,
stepsToReproduce, verification, humanVerified
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(insertQuery, [
bug.id,
bug.status,
bug.priority,
bug.dateReported,
bug.component,
bug.title,
bug.description,
bug.expectedBehavior,
bug.actualBehavior,
bug.potentialRootCause,
JSON.stringify(bug.filesLikelyInvolved),
JSON.stringify(bug.stepsToReproduce),
JSON.stringify(bug.verification),
bug.humanVerified ? 1 : 0
], async (insertErr) => {
if (insertErr) {
reject(new Error(`Failed to create bug: ${insertErr.message}`));
return;
}
// Record token usage
const inputText = JSON.stringify(args);
const outputText = `Bug ${bug.id} created successfully.`;
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'create_bug');
resolve(`${outputText}${formatTokenUsage(tokenUsage)}`);
});
}
catch (err) {
reject(new Error(`Bug creation failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
}
});
}
/**
* List bugs with filtering options
*/
async listBugs(db, args) {
this.tokenTracker.startOperation('list_bugs');
let query = 'SELECT * FROM bugs';
const params = [];
const conditions = [];
if (args.status) {
conditions.push('status = ?');
params.push(args.status);
}
if (args.priority) {
conditions.push('priority = ?');
params.push(args.priority);
}
if (args.component) {
conditions.push('component = ?');
params.push(args.component);
}
if (args.humanVerified !== undefined) {
conditions.push('humanVerified = ?');
params.push(args.humanVerified ? 1 : 0);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY dateReported DESC';
return new Promise((resolve, reject) => {
db.all(query, params, (err, rows) => {
if (err) {
reject(new Error(`Failed to list bugs: ${err.message}`));
}
else {
const bugs = rows.map(row => ({
...row,
filesLikelyInvolved: JSON.parse(row.filesLikelyInvolved || '[]'),
stepsToReproduce: JSON.parse(row.stepsToReproduce || '[]'),
verification: JSON.parse(row.verification || '[]'),
humanVerified: row.humanVerified === 1
}));
// Record token usage
const inputText = JSON.stringify(args);
const outputText = formatBugs(bugs);
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'list_bugs');
resolve(`${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`);
}
});
});
}
/**
* Update bug status
*/
async updateBugStatus(db, args) {
this.tokenTracker.startOperation('update_bug_status');
// Added comment to test context change detection
const { itemId, status, humanVerified } = args;
const validStatuses = ['Open', 'In Progress', 'Fixed', 'Closed', 'Temporarily Resolved'];
if (!validStatuses.includes(status)) {
throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`);
}
return new Promise((resolve, reject) => {
let updateQuery = 'UPDATE bugs SET status = ?';
const params = [status];
if (humanVerified !== undefined) {
updateQuery += ', humanVerified = ?';
params.push(humanVerified ? 1 : 0);
}
updateQuery += ' WHERE id = ?';
params.push(itemId);
db.run(updateQuery, params, (err, result) => {
if (err) {
reject(new Error(`Failed to update bug status: ${err.message}`));
}
else if (result && result.changes === 0) {
reject(new Error(`Bug ${itemId} not found`));
}
else {
// Record token usage
const inputText = JSON.stringify(args);
const outputText = `Updated bug ${itemId} to ${status}`;
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'update_bug_status');
resolve(`Bug ${itemId} updated to ${status}.\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`);
}
});
});
}
/**
* Search bugs
*/
async searchBugs(db, query, args) {
this.tokenTracker.startOperation('search_bugs');
const searchFields = args.searchFields || ['title', 'description', 'component'];
const limit = args.limit || 50;
const offset = args.offset || 0;
let sql = 'SELECT * FROM bugs WHERE ';
const conditions = [];
const params = [];
// Add search conditions
if (query) {
const searchConditions = searchFields.map((field) => `${field} LIKE ?`);
conditions.push(`(${searchConditions.join(' OR ')})`);
searchFields.forEach(() => params.push(`%${query}%`));
}
// Add filters
if (args.status) {
if (Array.isArray(args.status)) {
conditions.push(`status IN (${args.status.map(() => '?').join(', ')})`);
params.push(...args.status);
}
else {
conditions.push('status = ?');
params.push(args.status);
}
}
if (args.priority) {
if (Array.isArray(args.priority)) {
conditions.push(`priority IN (${args.priority.map(() => '?').join(', ')})`);
params.push(...args.priority);
}
else {
conditions.push('priority = ?');
params.push(args.priority);
}
}
if (args.component) {
conditions.push('component LIKE ?');
params.push(`%${args.component}%`);
}
if (args.humanVerified !== undefined) {
conditions.push('humanVerified = ?');
params.push(args.humanVerified ? 1 : 0);
}
// Date range filter
if (args.dateFrom) {
conditions.push('dateReported >= ?');
params.push(args.dateFrom);
}
if (args.dateTo) {
conditions.push('dateReported <= ?');
params.push(args.dateTo);
}
if (conditions.length === 0) {
sql = 'SELECT * FROM bugs';
}
else {
sql += conditions.join(' AND ');
}
// Add sorting
const sortBy = args.sortBy || 'dateReported';
const sortOrder = args.sortOrder || 'desc';
sql += ` ORDER BY ${sortBy} ${sortOrder}`;
// Add pagination
sql += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
return new Promise((resolve, reject) => {
db.all(sql, params, (err, rows) => {
if (err) {
reject(new Error(`Failed to search bugs: ${err.message}`));
}
else {
const bugs = rows.map(row => ({
...row,
type: 'bug',
filesLikelyInvolved: JSON.parse(row.filesLikelyInvolved || '[]'),
stepsToReproduce: JSON.parse(row.stepsToReproduce || '[]'),
verification: JSON.parse(row.verification || '[]'),
humanVerified: row.humanVerified === 1
}));
resolve(bugs);
}
});
});
}
/**
* Bulk update bug status
*/
async bulkUpdateBugStatus(db, args) {
this.tokenTracker.startOperation('bulk_update_bug_status');
const { updates } = args;
const results = [];
for (const update of updates) {
try {
const result = await this.updateBugStatus(db, update);
results.push({
status: 'success',
bugId: update.itemId,
message: `Updated to ${update.status}`,
humanVerified: update.humanVerified
});
}
catch (error) {
results.push({
status: 'error',
bugId: update.itemId,
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// Record token usage
const inputText = JSON.stringify(args);
const outputText = formatBulkUpdateResults(results, 'bugs');
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'bulk_update_bug_status');
return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`;
}
/**
* Generate next ID for bug with retry mechanism for race conditions
*/
async generateNextIdWithRetry(db, retryCount = 0) {
const maxRetries = 3;
try {
return await this.generateNextId(db, 'bug');
}
catch (error) {
if (retryCount < maxRetries && error instanceof Error && error.message.includes('UNIQUE constraint')) {
// Wait a bit and retry
await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 50));
return this.generateNextIdWithRetry(db, retryCount + 1);
}
throw error;
}
}
/**
* Generate next ID for bug
*/
async generateNextId(db, type) {
return new Promise((resolve, reject) => {
db.get('SELECT MAX(CAST(SUBSTR(id, 6) AS INTEGER)) as maxId FROM bugs', [], (err, row) => {
if (err) {
reject(new Error(`Failed to generate ID: ${err.message}`));
}
else {
const nextNum = (row?.maxId || 0) + 1;
resolve(`Bug #${nextNum.toString().padStart(3, '0')}`);
}
});
});
}
}
//# sourceMappingURL=bugs.js.map