error-handling.ts•8.57 kB
/**
* Error Handling Utilities
* Provides standardized error handling and logging for the Figma MCP Server
*/
export enum ErrorType {
VALIDATION_ERROR = "VALIDATION_ERROR",
API_ERROR = "API_ERROR",
NETWORK_ERROR = "NETWORK_ERROR",
PROCESSING_ERROR = "PROCESSING_ERROR",
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR",
RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR",
NOT_FOUND = "NOT_FOUND",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
export interface ErrorContext {
operation: string;
input?: any;
timestamp: string;
requestId?: string;
fileKey?: string;
nodeId?: string;
}
/**
* Log levels for controlling output verbosity
*/
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
/**
* Logger configuration
*/
interface LoggerConfig {
level: LogLevel;
enableColors: boolean;
enableTimestamps: boolean;
}
/**
* Enhanced logger interface with proper formatting and log levels
*/
class Logger {
private config: LoggerConfig;
constructor() {
this.config = {
level: process.env.LOG_LEVEL
? parseInt(process.env.LOG_LEVEL)
: process.env.NODE_ENV === "development"
? LogLevel.DEBUG
: LogLevel.INFO,
enableColors: process.env.NO_COLOR !== "true",
enableTimestamps: true,
};
}
private shouldLog(level: LogLevel): boolean {
return level >= this.config.level;
}
private formatMessage(level: string, message: string, context?: any): string {
const timestamp = this.config.enableTimestamps
? new Date().toISOString()
: "";
const contextStr = context
? ` | Context: ${JSON.stringify(context, null, 2)}`
: "";
return `[${level}] ${timestamp}: ${message}${contextStr}`;
}
private getColorCode(level: string): string {
if (!this.config.enableColors) return "";
switch (level) {
case "DEBUG":
return "\x1b[36m"; // Cyan
case "INFO":
return "\x1b[32m"; // Green
case "WARN":
return "\x1b[33m"; // Yellow
case "ERROR":
return "\x1b[31m"; // Red
default:
return "";
}
}
private resetColor(): string {
return this.config.enableColors ? "\x1b[0m" : "";
}
debug(message: string, context?: any): void {
if (!this.shouldLog(LogLevel.DEBUG)) return;
const colorCode = this.getColorCode("DEBUG");
const resetCode = this.resetColor();
const formattedMessage = this.formatMessage("DEBUG", message, context);
//console.debug(`${colorCode}${formattedMessage}${resetCode}`);
}
info(message: string, context?: any): void {
if (!this.shouldLog(LogLevel.INFO)) return;
const colorCode = this.getColorCode("INFO");
const resetCode = this.resetColor();
const formattedMessage = this.formatMessage("INFO", message, context);
//console.error(`${formattedMessage}`);
}
warn(message: string, context?: any): void {
if (!this.shouldLog(LogLevel.WARN)) return;
const colorCode = this.getColorCode("WARN");
const resetCode = this.resetColor();
const formattedMessage = this.formatMessage("WARN", message, context);
//console.warn(`${colorCode}${formattedMessage}${resetCode}`);
}
error(message: string, context?: any): void {
if (!this.shouldLog(LogLevel.ERROR)) return;
const colorCode = this.getColorCode("ERROR");
const resetCode = this.resetColor();
const formattedMessage = this.formatMessage("ERROR", message, context);
console.error(`${colorCode}${formattedMessage}${resetCode}`);
}
/**
* Log performance metrics
*/
performance(operation: string, duration: number, context?: any): void {
this.info(`Performance: ${operation} completed in ${duration}ms`, context);
}
/**
* Log API requests
*/
apiRequest(method: string, url: string, context?: any): void {
this.debug(`API Request: ${method} ${url}`, context);
}
/**
* Log API responses
*/
apiResponse(
method: string,
url: string,
status: number,
duration: number,
context?: any
): void {
const level = status >= 400 ? "warn" : "debug";
this[level](
`API Response: ${method} ${url} - ${status} (${duration}ms)`,
context
);
}
/**
* Log memory usage
*/
memory(operation: string): void {
if (!this.shouldLog(LogLevel.DEBUG)) return;
const usage = process.memoryUsage();
this.debug(`Memory usage after ${operation}`, {
rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(usage.external / 1024 / 1024)}MB`,
});
}
/**
* Set log level dynamically
*/
setLevel(level: LogLevel): void {
this.config.level = level;
}
/**
* Get current log level
*/
getLevel(): LogLevel {
return this.config.level;
}
}
export const logger = new Logger();
export class FigmaServerError extends Error {
public readonly type: ErrorType;
public readonly context: ErrorContext;
public readonly statusCode: number;
constructor(
message: string,
type: ErrorType,
context: ErrorContext,
statusCode: number = 500
) {
super(message);
this.name = "FigmaServerError";
this.type = type;
this.context = context;
this.statusCode = statusCode;
}
}
/**
* Creates a standardized error response for MCP tools
*/
export const createErrorResponse = (
error: Error | FigmaServerError,
operation: string
) => {
let errorType = ErrorType.UNKNOWN_ERROR;
let statusCode = 500;
if (error instanceof FigmaServerError) {
errorType = error.type;
statusCode = error.statusCode;
}
logger.error(`Error in ${operation}:`, {
type: errorType,
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
return {
isError: true,
content: [
{
type: "text" as const,
text: `Error: ${error.message}`,
},
],
};
};
/**
* Wraps async operations with error handling
*/
export const withErrorHandling = <T extends any[], R>(
operation: string,
fn: (...args: T) => Promise<R>
) => {
return async (...args: T): Promise<R> => {
try {
return await fn(...args);
} catch (error) {
if (error instanceof FigmaServerError) {
throw error;
}
// Convert unknown errors to FigmaServerError
throw new FigmaServerError(
error instanceof Error ? error.message : "Unknown error occurred",
ErrorType.UNKNOWN_ERROR,
{
operation,
input: args,
timestamp: new Date().toISOString(),
}
);
}
};
};
/**
* Handles Axios errors specifically
*/
export const handleAxiosError = (error: any, operation: string): never => {
if (error.response) {
// Server responded with error status
const status = error.response.status;
let errorType = ErrorType.API_ERROR;
switch (status) {
case 401:
case 403:
errorType = ErrorType.AUTHENTICATION_ERROR;
break;
case 429:
errorType = ErrorType.RATE_LIMIT_ERROR;
break;
case 400:
errorType = ErrorType.VALIDATION_ERROR;
break;
}
throw new FigmaServerError(
`Figma API error: ${error.response.data?.message || error.message}`,
errorType,
{
operation,
timestamp: new Date().toISOString(),
},
status
);
} else if (error.request) {
// Network error
throw new FigmaServerError(
"Network error: Unable to reach Figma API",
ErrorType.NETWORK_ERROR,
{
operation,
timestamp: new Date().toISOString(),
},
503
);
} else {
// Other error
throw new FigmaServerError(
error.message || "Unknown error occurred",
ErrorType.UNKNOWN_ERROR,
{
operation,
timestamp: new Date().toISOString(),
}
);
}
};
/**
* Timeout wrapper for promises
*/
export const withTimeout = <T>(
promise: Promise<T>,
timeoutMs: number,
operation: string
): Promise<T> => {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(
new FigmaServerError(
`Operation timed out after ${timeoutMs}ms`,
ErrorType.NETWORK_ERROR,
{
operation,
timestamp: new Date().toISOString(),
},
408
)
);
}, timeoutMs);
}),
]);
};