mcp-product-manager
Version:
MCP Orchestrator for task and project management with web interface
176 lines • 6.84 kB
JavaScript
import ErrorResponseBuilder from '../../api/utils/errorResponseBuilder.js';
import db from '../utils/database.js';
const errorResponseBuilder = new ErrorResponseBuilder();
// Track retry attempts per request
const retryTracker = new Map();
function getRetryCount(requestKey) {
const count = retryTracker.get(requestKey) || 0;
return count;
}
function incrementRetryCount(requestKey) {
const current = getRetryCount(requestKey);
retryTracker.set(requestKey, current + 1);
// Clean up old entries after 5 minutes
setTimeout(() => {
retryTracker.delete(requestKey);
}, 5 * 60 * 1000);
return current + 1;
}
function getRequestKey(req) {
// Create a unique key based on agent, operation, and main parameters
const agent = req.headers['x-agent-name'] || req.body?.agent || 'unknown';
const operation = req.path + ':' + req.method;
const resourceId = req.params?.id || req.body?.task_id || req.body?.project || '';
return `${agent}:${operation}:${resourceId}`;
}
export const errorHandler = async (err, req, res, next) => {
// Skip if response already sent
if (res.headersSent) {
next(err);
return;
}
const requestKey = getRequestKey(req);
const retryCount = err.retry ? incrementRetryCount(requestKey) : getRetryCount(requestKey);
// Build context for error response
const context = {
agentName: req.headers['x-agent-name'] || req.body?.agent || 'unknown',
agentType: req.headers['x-agent-type'] || req.agentType || 'unknown',
retryCount: retryCount,
operation: `${req.method} ${req.path}`,
project: req.params?.project || req.body?.project || null,
requestId: req.id || null,
requiredPermission: err.requiredPermission || null,
validationErrors: err.validationErrors || null,
failedTasks: err.failedTasks || null
};
// Map common errors to our error codes
let mappedError = err;
if (err.message?.includes('Permission denied') || err.message?.includes('403')) {
mappedError = { ...err, code: 'PERMISSION_DENIED' };
}
else if (err.message?.includes('not found') || err.message?.includes('404')) {
if (err.message.includes('task')) {
mappedError = { ...err, code: 'TASK_NOT_FOUND' };
}
else if (err.message.includes('project')) {
mappedError = { ...err, code: 'PROJECT_NOT_FOUND' };
}
}
else if (err.message?.includes('Database') || err.message?.includes('SQLITE')) {
mappedError = { ...err, code: 'DATABASE_ERROR' };
}
else if (err.message?.includes('already claimed') || err.message?.includes('concurrent')) {
mappedError = { ...err, code: 'CONCURRENT_CLAIM' };
}
else if (err.message?.includes('bundle') && err.message?.includes('claim')) {
mappedError = { ...err, code: 'BUNDLE_CLAIM_FAILED' };
}
else if (err.message?.includes('validation') || err.message?.includes('Invalid')) {
mappedError = { ...err, code: 'VALIDATION_ERROR' };
}
// Build the error response
const errorResponse = errorResponseBuilder.build(mappedError, context);
// Log error pattern to database
await logErrorPattern(mappedError, context, errorResponse);
// Send response
res.status(errorResponse.metadata.httpStatus).json(errorResponse);
};
async function logErrorPattern(error, context, response) {
try {
await db.run(`
INSERT INTO error_patterns (
error_id,
error_code,
agent_name,
agent_type,
retry_count,
operation,
project,
timestamp,
http_status,
category
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
response.metadata.errorId,
response.error.code,
context.agentName,
context.agentType,
context.retryCount,
context.operation,
context.project,
response.error.timestamp,
response.metadata.httpStatus,
response.error.category
]);
// Check for intervention patterns (5+ same errors)
const recentErrors = await db.query(`
SELECT COUNT(*) as count
FROM error_patterns
WHERE error_code = ?
AND agent_name = ?
AND timestamp > datetime('now', '-30 minutes')
`, [response.error.code, context.agentName]);
const errorCount = recentErrors[0]?.count;
if (errorCount && errorCount >= 5) {
// Flag for intervention
console.log(`INTERVENTION NEEDED: Agent ${context.agentName} has ${errorCount} ${response.error.code} errors`);
}
}
catch (dbError) {
// Don't fail the main error handler if logging fails
console.error('Failed to log error pattern:', dbError);
}
}
// Express error wrapper for async routes
export const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Permission checking middleware
export const requirePermission = (permission) => {
return (req, res, next) => {
const agentType = req.headers['x-agent-type'] || req.agentType || 'unknown';
const permissions = {
orchestrator: ['orch_read', 'orch_write', 'agent_read', 'shared_read', 'shared_write'],
basic: ['agent_read', 'agent_write', 'shared_read', 'shared_write'],
bundle: ['agent_read', 'agent_write', 'shared_read', 'shared_write'],
critical: ['agent_read', 'agent_write', 'shared_read', 'shared_write'],
maintenance: ['agent_read', 'agent_write', 'shared_read', 'shared_write']
};
if (!permissions[agentType]?.includes(permission)) {
const error = new Error(`Permission denied: ${agentType} agents cannot access ${permission} operations`);
error.code = 'PERMISSION_DENIED';
error.requiredPermission = permission;
return next(error);
}
next();
};
};
// 404 handler
export const notFoundHandler = (req, res) => {
const context = {
agentName: req.headers['x-agent-name'] || 'unknown',
agentType: req.headers['x-agent-type'] || 'unknown',
operation: `${req.method} ${req.path}`,
retryCount: 0,
project: null,
requestId: null,
requiredPermission: null,
validationErrors: null,
failedTasks: null
};
const error = {
code: 'ENDPOINT_NOT_FOUND',
message: `Endpoint ${req.path} not found`
};
const errorResponse = errorResponseBuilder.build(error, context);
res.status(404).json(errorResponse);
};
export default {
errorHandler,
asyncHandler,
requirePermission,
notFoundHandler
};
//# sourceMappingURL=errorHandler.js.map