Skip to main content
Glama
error-mapper.ts16 kB
/** * Error Mapper * * Maps various error types to normalized MCP format while * preserving original error details for debugging. */ import { z } from "zod"; import { MCPError, MCPErrorCode, MCPErrorData, FreshBooksErrorCode, FreshBooksApiError, ErrorMapping, ErrorContext, OAuthError, } from "./types.js"; /** * Error Mapper Class * * Provides static methods for mapping different error types * to the normalized MCP error format. */ export class ErrorMapper { /** * Map FreshBooks API error to MCP error * * @param fbError - FreshBooks API error object * @param context - Additional error context * @returns Normalized MCP error */ static mapFreshBooksError( fbError: FreshBooksApiError, context?: ErrorContext ): MCPError { const errorCode = fbError.error?.code || "UNKNOWN"; const mapping = ERROR_MAPPINGS[errorCode] || DEFAULT_MAPPING; const mcpError: MCPError = Object.assign( new Error(mapping.getMessage(fbError.error)), { code: mapping.mcpCode, message: mapping.getMessage(fbError.error), data: { freshbooksError: { code: fbError.error?.code || "UNKNOWN", message: fbError.error?.message || "Unknown error", field: fbError.error?.field, errno: fbError.error?.errno, details: fbError.error?.details, }, context, recoverable: mapping.recoverable, suggestion: mapping.getSuggestion(fbError.error), retryAfter: fbError.retryAfter, } as MCPErrorData, } ); return mcpError; } /** * Map Zod validation error to MCP error * * @param zodError - Zod validation error * @param context - Additional error context * @returns Normalized MCP error */ static mapValidationError( zodError: z.ZodError, context?: ErrorContext ): MCPError { const issues = zodError.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code, expected: "expected" in issue ? String(issue.expected) : undefined, received: "received" in issue ? issue.received : undefined, })); const firstIssue = issues[0]; const message = firstIssue ? `Validation failed for "${firstIssue.path}": ${firstIssue.message}` : "Validation failed"; const mcpError: MCPError = Object.assign(new Error(message), { code: MCPErrorCode.INVALID_PARAMS, message, data: { validationErrors: issues, context, recoverable: true, suggestion: this.getValidationSuggestion(issues), statusCode: 400, } as MCPErrorData, }); return mcpError; } /** * Map OAuth error to MCP error * * @param error - OAuth error * @param context - Additional error context * @returns Normalized MCP error */ static mapOAuthError(error: OAuthError, context?: ErrorContext): MCPError { const isTokenExpired = error.code === "invalid_grant" || error.code === "token_expired" || error.message.toLowerCase().includes("expired"); const isInvalidAuth = error.code === "invalid_client" || error.code === "unauthorized_client" || error.code === "access_denied"; if (isTokenExpired) { const message = "Authentication token has expired."; return Object.assign(new Error(message), { code: MCPErrorCode.TOKEN_EXPIRED, message, data: { freshbooksError: { code: error.code, message: error.message, }, context, recoverable: true, suggestion: "Call auth_refresh to get a new token, or re-authenticate using auth_get_url", } as MCPErrorData, }); } if (isInvalidAuth) { const message = "Authentication failed or access was denied."; return Object.assign(new Error(message), { code: MCPErrorCode.NOT_AUTHENTICATED, message, data: { freshbooksError: { code: error.code, message: error.message, }, context, recoverable: true, suggestion: "Re-authenticate using auth_get_url to obtain valid credentials", } as MCPErrorData, }); } const message = `OAuth error: ${error.message}`; return Object.assign(new Error(message), { code: MCPErrorCode.NOT_AUTHENTICATED, message, data: { freshbooksError: { code: error.code, message: error.message, details: error.details, }, context, recoverable: true, suggestion: "Check your OAuth credentials and try again", } as MCPErrorData, }); } /** * Get validation suggestion based on issues * * @param issues - Validation issues * @returns Human-readable suggestion */ private static getValidationSuggestion( issues: Array<{ path: string; message: string }> ): string { if (issues.length === 0) { return "Check the input parameters and try again"; } if (issues.length === 1) { const issue = issues[0]; if (!issue) { return "Check the input parameters and try again"; } return `Fix the "${issue.path}" field: ${issue.message.toLowerCase()}`; } const fields = issues.map((i) => `"${i.path}"`).join(", "); return `Fix the following fields: ${fields}`; } /** * Map network/transport error to MCP error * * Handles various network error conditions like timeouts, * connection failures, DNS errors, etc. * * @param error - Network error * @param context - Additional error context * @returns Normalized MCP error */ static mapNetworkError(error: Error, context?: ErrorContext): MCPError { const errorMessage = error.message.toLowerCase(); // Timeout errors const isTimeout = errorMessage.includes("etimedout") || errorMessage.includes("timeout"); // Connection errors const isConnection = errorMessage.includes("econnrefused") || errorMessage.includes("enotfound") || errorMessage.includes("econnreset"); // DNS errors const isDNS = errorMessage.includes("eai_again") || errorMessage.includes("getaddrinfo"); // Socket errors const isSocket = errorMessage.includes("socket hang up"); if (isTimeout) { const message = "Request timed out. FreshBooks may be slow to respond."; return Object.assign(new Error(message), { code: MCPErrorCode.SERVICE_UNAVAILABLE, message, data: { context, recoverable: true, suggestion: "Wait a moment and try again. If the issue persists, check FreshBooks status.", } as MCPErrorData, }); } if (isConnection || isDNS) { const message = "Could not connect to FreshBooks API."; return Object.assign(new Error(message), { code: MCPErrorCode.SERVICE_UNAVAILABLE, message, data: { context, recoverable: true, suggestion: "Check your internet connection and try again.", } as MCPErrorData, }); } if (isSocket) { const message = "Connection was closed unexpectedly."; return Object.assign(new Error(message), { code: MCPErrorCode.SERVICE_UNAVAILABLE, message, data: { context, recoverable: true, suggestion: "The connection was interrupted. Please try again.", } as MCPErrorData, }); } // Generic network error const message = `Network error: ${error.message}`; return Object.assign(new Error(message), { code: MCPErrorCode.INTERNAL_ERROR, message, data: { context, recoverable: true, suggestion: "An unexpected network error occurred. Please try again.", } as MCPErrorData, }); } /** * Map HTTP status code to MCP error * * @param statusCode - HTTP status code * @param statusText - HTTP status text * @param context - Additional error context * @returns Normalized MCP error */ static mapHttpError( statusCode: number, statusText: string, context?: ErrorContext ): MCPError { let code: MCPErrorCode; let message: string; let recoverable: boolean; let suggestion: string; switch (statusCode) { case 401: code = MCPErrorCode.NOT_AUTHENTICATED; message = "Authentication required or session expired"; recoverable = true; suggestion = "Please re-authenticate using auth_get_url"; break; case 403: code = MCPErrorCode.PERMISSION_DENIED; message = "Permission denied"; recoverable = false; suggestion = "You don't have access to this resource. Contact your administrator."; break; case 404: code = MCPErrorCode.RESOURCE_NOT_FOUND; message = "Resource not found"; recoverable = false; suggestion = "Verify the resource ID is correct. The resource may have been deleted."; break; case 409: code = MCPErrorCode.CONFLICT; message = "Conflict with existing resource"; recoverable = false; suggestion = "A resource with these details already exists"; break; case 422: code = MCPErrorCode.VALIDATION_ERROR; message = "Validation failed"; recoverable = true; suggestion = "Check the input values and try again"; break; case 429: code = MCPErrorCode.RATE_LIMITED; message = "Rate limit exceeded"; recoverable = true; suggestion = "Wait a moment before making more requests"; break; case 500: case 502: case 503: case 504: code = MCPErrorCode.SERVICE_UNAVAILABLE; message = "FreshBooks service is unavailable"; recoverable = true; suggestion = "An error occurred on FreshBooks' side. Please try again."; break; default: code = MCPErrorCode.INTERNAL_ERROR; message = `HTTP error ${statusCode}: ${statusText}`; recoverable = true; suggestion = "An unexpected error occurred. Please try again."; } return Object.assign(new Error(message), { code, message, data: { freshbooksError: { code: `HTTP_${statusCode}`, message: statusText, }, context, recoverable, suggestion, } as MCPErrorData, }); } } /** * Error Mapping Configuration * * Maps FreshBooks error codes to MCP error codes with * recovery guidance. */ const ERROR_MAPPINGS: Record<string, ErrorMapping> = { // Authentication Errors [FreshBooksErrorCode.UNAUTHORIZED]: { mcpCode: MCPErrorCode.NOT_AUTHENTICATED, recoverable: true, statusCode: 401, getMessage: () => "Authentication required or session expired", getSuggestion: () => "Please authenticate using auth_get_url to obtain access credentials", }, [FreshBooksErrorCode.UNAUTHENTICATED]: { mcpCode: MCPErrorCode.NOT_AUTHENTICATED, recoverable: true, statusCode: 401, getMessage: () => "No valid authentication found", getSuggestion: () => "Call auth_get_url to start the authentication process", }, [FreshBooksErrorCode.TOKEN_EXPIRED]: { mcpCode: MCPErrorCode.TOKEN_EXPIRED, recoverable: true, statusCode: 401, getMessage: () => "Access token has expired", getSuggestion: () => "Call auth_refresh to refresh your token, or re-authenticate if refresh fails", }, [FreshBooksErrorCode.INVALID_GRANT]: { mcpCode: MCPErrorCode.TOKEN_EXPIRED, recoverable: true, statusCode: 401, getMessage: () => "Refresh token is invalid or expired", getSuggestion: () => "Re-authenticate using auth_get_url to obtain new credentials", }, // Permission Errors [FreshBooksErrorCode.FORBIDDEN]: { mcpCode: MCPErrorCode.PERMISSION_DENIED, recoverable: false, statusCode: 403, getMessage: (e) => `Permission denied: ${e.message}`, getSuggestion: () => "You don't have permission to access this resource. Contact your FreshBooks administrator to request access.", }, [FreshBooksErrorCode.INSUFFICIENT_PERMISSIONS]: { mcpCode: MCPErrorCode.PERMISSION_DENIED, recoverable: false, statusCode: 403, getMessage: (e) => `Insufficient permissions: ${e.message}`, getSuggestion: () => "Your FreshBooks account lacks the required permissions for this operation", }, // Resource Errors [FreshBooksErrorCode.NOT_FOUND]: { mcpCode: MCPErrorCode.RESOURCE_NOT_FOUND, recoverable: false, statusCode: 404, getMessage: (e) => { if (e.field) { return `Resource not found: The ${e.field} you specified doesn't exist`; } return `Resource not found: ${e.message}`; }, getSuggestion: (e) => { if (e.field) { return `Verify the ${e.field} is correct. The resource may have been deleted or moved to a different account.`; } return "Double-check the ID or identifier. The resource may have been deleted."; }, }, // Validation Errors [FreshBooksErrorCode.VALIDATION_ERROR]: { mcpCode: MCPErrorCode.VALIDATION_ERROR, recoverable: true, statusCode: 422, getMessage: (e) => { if (e.field) { return `Invalid value for "${e.field}": ${e.message}`; } return `Validation error: ${e.message}`; }, getSuggestion: (e) => { if (e.field) { return `Check the value provided for "${e.field}" and ensure it meets the requirements`; } return "Review the input values and ensure they match the expected format"; }, }, [FreshBooksErrorCode.BAD_REQUEST]: { mcpCode: MCPErrorCode.VALIDATION_ERROR, recoverable: true, statusCode: 400, getMessage: (e) => `Invalid request: ${e.message}`, getSuggestion: () => "Check the request parameters and ensure all required fields are provided correctly", }, // Rate Limiting [FreshBooksErrorCode.RATE_LIMIT_EXCEEDED]: { mcpCode: MCPErrorCode.RATE_LIMITED, recoverable: true, statusCode: 429, getMessage: () => "Rate limit exceeded", getSuggestion: () => "You've made too many requests in a short period. Wait before making more requests.", }, // Conflict Errors [FreshBooksErrorCode.CONFLICT]: { mcpCode: MCPErrorCode.CONFLICT, recoverable: false, statusCode: 409, getMessage: (e) => `Conflict: ${e.message}`, getSuggestion: () => "A resource with these details already exists. Try updating the existing resource instead of creating a new one.", }, // Server Errors [FreshBooksErrorCode.INTERNAL_ERROR]: { mcpCode: MCPErrorCode.INTERNAL_ERROR, recoverable: true, statusCode: 500, getMessage: () => "FreshBooks server error", getSuggestion: () => "An error occurred on FreshBooks' side. Please try again in a few moments. If the issue persists, contact FreshBooks support.", }, [FreshBooksErrorCode.SERVICE_UNAVAILABLE]: { mcpCode: MCPErrorCode.SERVICE_UNAVAILABLE, recoverable: true, statusCode: 503, getMessage: () => "FreshBooks service is temporarily unavailable", getSuggestion: () => "FreshBooks may be undergoing maintenance. Please try again later or check status.freshbooks.com", }, }; /** * Default Error Mapping * * Used when no specific mapping is found for an error code. */ const DEFAULT_MAPPING: ErrorMapping = { mcpCode: MCPErrorCode.INTERNAL_ERROR, recoverable: true, getMessage: (e) => e.message || "An unexpected error occurred", getSuggestion: () => "Please try again. If the issue persists, check the error details.", };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Good-Samaritan-Software-LLC/freshbooks-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server