/**
* Standardized error handling for Local Explorer MCP
*
* Provides consistent error codes, structured error objects, and formatted error messages
* across all tools. Enables better debugging, logging, and error recovery.
*
* @module errors
*/
/**
* Standard error codes for all tools
* Format: E{category}{number}
* - 1xxx: Path/Security errors
* - 2xxx: Resource errors
* - 3xxx: Validation errors
* - 4xxx: Execution errors
* - 5xxx: System errors
*/
export enum ErrorCode {
// Path/Security errors (1xxx)
PATH_OUTSIDE_WORKSPACE = 'E1001',
PATH_BLOCKED_PATTERN = 'E1002',
PATH_NOT_FOUND = 'E1003',
PATH_PERMISSION_DENIED = 'E1004',
PATH_INVALID = 'E1005',
// Resource errors (2xxx)
MEMORY_LIMIT_EXCEEDED = 'E2001',
TOKEN_LIMIT_EXCEEDED = 'E2002',
FILE_TOO_LARGE = 'E2003',
OUTPUT_TOO_LARGE = 'E2004',
TIMEOUT = 'E2005',
// Validation errors (3xxx)
INVALID_PATTERN = 'E3001',
INVALID_COMMAND = 'E3002',
INVALID_PARAMETER = 'E3003',
MISSING_REQUIRED_PARAMETER = 'E3004',
INVALID_OPERATION = 'E3005',
// Execution errors (4xxx)
COMMAND_FAILED = 'E4001',
COMMAND_NOT_FOUND = 'E4002',
OPERATION_FAILED = 'E4003',
READ_FAILED = 'E4004',
WRITE_FAILED = 'E4005',
// System errors (5xxx)
INTERNAL_ERROR = 'E5001',
NOT_IMPLEMENTED = 'E5002',
UNKNOWN_ERROR = 'E5999',
}
/**
* Structured error object with code, message, hints, and context
*/
export interface ToolError {
/** Standard error code */
code: ErrorCode;
/** Human-readable error message */
message: string;
/** Actionable hints for fixing the error */
hints?: string[];
/** Additional details for debugging */
details?: Record<string, unknown>;
/** Original error that caused this error */
cause?: Error;
}
/**
* Builder for constructing ToolError objects with fluent API
*
* @example
* ```typescript
* const error = new ToolErrorBuilder(
* ErrorCode.PATH_NOT_FOUND,
* 'File not found'
* )
* .withHints('Check file path', 'Verify permissions')
* .withDetails({ path: '/foo/bar.txt' })
* .build();
* ```
*/
export class ToolErrorBuilder {
private error: ToolError;
constructor(code: ErrorCode, message: string) {
this.error = { code, message, hints: [], details: {} };
}
/**
* Add actionable hints for fixing the error
*/
withHints(...hints: string[]): this {
this.error.hints = hints;
return this;
}
/**
* Add additional details for debugging
*/
withDetails(details: Record<string, unknown>): this {
this.error.details = details;
return this;
}
/**
* Add the original error that caused this error
*/
withCause(cause: Error): this {
this.error.cause = cause;
return this;
}
/**
* Build the final ToolError object
*/
build(): ToolError {
return this.error;
}
}
/**
* Standard result type for all tool operations
* Either success with data, or failure with structured error
*/
export type ToolResult<T> =
| { success: true; data: T; warnings?: string[] }
| { success: false; error: ToolError };
/**
* Create a success result
*
* @param data - Result data
* @param warnings - Optional warnings (non-fatal issues)
* @returns Success result
*/
export function createSuccessResult<T>(
data: T,
warnings?: string[]
): ToolResult<T> {
return warnings && warnings.length > 0
? { success: true, data, warnings }
: { success: true, data };
}
/**
* Create an error result
*
* @param error - Structured error object
* @returns Error result
*/
export function createErrorResult<T>(error: ToolError): ToolResult<T> {
return { success: false, error };
}
/**
* Format a ToolError as a human-readable string
*
* @param error - Error to format
* @returns Formatted error message
*/
export function formatToolError(error: ToolError): string {
const parts = [`ERROR [${error.code}]: ${error.message}`];
if (error.hints && error.hints.length > 0) {
parts.push('\nSuggestions:');
error.hints.forEach((hint, i) => parts.push(` ${i + 1}. ${hint}`));
}
if (error.details && Object.keys(error.details).length > 0) {
parts.push('\nDetails:');
Object.entries(error.details).forEach(([key, value]) => {
parts.push(` ${key}: ${JSON.stringify(value)}`);
});
}
if (error.cause) {
parts.push(`\nCause: ${error.cause.message}`);
}
return parts.join('\n');
}
/**
* Convert an unknown error to a ToolError
*
* @param error - Unknown error (could be Error, string, or anything)
* @param defaultCode - Default error code to use
* @returns Structured ToolError
*/
export function toToolError(
error: unknown,
defaultCode: ErrorCode = ErrorCode.UNKNOWN_ERROR
): ToolError {
if (error instanceof Error) {
return {
code: defaultCode,
message: error.message,
cause: error
};
}
if (typeof error === 'string') {
return {
code: defaultCode,
message: error
};
}
return {
code: defaultCode,
message: String(error)
};
}
/**
* Common error factory functions for frequently used errors
*/
export const CommonErrors = {
/**
* Create a path not found error
*/
pathNotFound(path: string): ToolError {
return new ToolErrorBuilder(
ErrorCode.PATH_NOT_FOUND,
`Path not found: ${path}`
)
.withHints(
'Verify the path exists',
'Check for typos in the path',
'Ensure you have read permissions'
)
.withDetails({ path })
.build();
},
/**
* Create a path outside workspace error
*/
pathOutsideWorkspace(path: string, allowedRoots: string[]): ToolError {
return new ToolErrorBuilder(
ErrorCode.PATH_OUTSIDE_WORKSPACE,
`Path is outside allowed workspace: ${path}`
)
.withHints(
'Path must be within workspace directories',
`Allowed roots: ${allowedRoots.join(', ')}`
)
.withDetails({ path, allowedRoots })
.build();
},
/**
* Create a permission denied error
*/
permissionDenied(path: string, operation: string): ToolError {
return new ToolErrorBuilder(
ErrorCode.PATH_PERMISSION_DENIED,
`Permission denied: Cannot ${operation} ${path}`
)
.withHints(
'Check file/directory permissions',
'Ensure you have the required access rights',
'Try running with appropriate permissions'
)
.withDetails({ path, operation })
.build();
},
/**
* Create a file too large error
*/
fileTooLarge(path: string, sizeKB: number, maxKB: number): ToolError {
return new ToolErrorBuilder(
ErrorCode.FILE_TOO_LARGE,
`File too large: ${sizeKB}KB (max: ${maxKB}KB)`
)
.withHints(
'Use pagination with charLength parameter',
'Use matchString for targeted extraction'
)
.withDetails({ path, sizeKB, maxKB })
.build();
},
/**
* Create a memory limit exceeded error
*/
memoryLimitExceeded(usedMB: number, limitMB: number): ToolError {
return new ToolErrorBuilder(
ErrorCode.MEMORY_LIMIT_EXCEEDED,
`Memory limit exceeded: ${usedMB}MB / ${limitMB}MB used`
)
.withHints(
'Server is under heavy load - try again shortly',
'Use smaller charLength values',
'Use filesOnly mode for discovery',
'Reduce number of concurrent operations'
)
.withDetails({ usedMB, limitMB, percentage: (usedMB / limitMB) * 100 })
.build();
},
/**
* Create a token limit exceeded error
*/
tokenLimitExceeded(tokens: number, maxTokens: number): ToolError {
return new ToolErrorBuilder(
ErrorCode.TOKEN_LIMIT_EXCEEDED,
`Response too large: ${tokens.toLocaleString()} tokens (max: ${maxTokens.toLocaleString()})`
)
.withHints(
'Use charLength parameter for pagination',
'Use more specific filters',
'Reduce the scope of the query'
)
.withDetails({ tokens, maxTokens })
.build();
},
/**
* Create a missing parameter error
*/
missingParameter(parameterName: string): ToolError {
return new ToolErrorBuilder(
ErrorCode.MISSING_REQUIRED_PARAMETER,
`Missing required parameter: ${parameterName}`
)
.withHints(`Provide a value for '${parameterName}'`)
.withDetails({ parameterName })
.build();
},
/**
* Create a command failed error
*/
commandFailed(command: string, exitCode: number, stderr: string): ToolError {
return new ToolErrorBuilder(
ErrorCode.COMMAND_FAILED,
`Command failed with exit code ${exitCode}: ${command}`
)
.withHints(
'Check command syntax',
'Verify all required tools are installed',
'Review error output for details'
)
.withDetails({ command, exitCode, stderr: stderr.substring(0, 500) })
.build();
}
};