error-handlers.ts•5.35 kB
/**
* Error Handling Utilities
*
* Provides functions to map various error types to MCP-compatible responses.
* All error messages follow the patterns defined in tool-contracts.md.
*/
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { ZodError } from "zod";
/**
* Error types for categorization
*/
export type ErrorType = "validation" | "api" | "network" | "not_found" | "rate_limit" | "unknown";
/**
* Structured error response
*/
export interface ErrorResponse {
type: ErrorType;
message: string;
details?: unknown;
statusCode?: number;
}
/**
* Create an MCP tool error result
*
* @param error - Error response object
* @returns MCP-compatible tool result with error flag
*/
export function createErrorResult(error: ErrorResponse): CallToolResult {
return {
content: [
{
type: "text",
text: JSON.stringify(error, null, 2),
},
],
isError: true,
};
}
/**
* Map Zod validation errors to MCP error result
*
* @param error - ZodError from validation failure
* @returns MCP tool result with validation error details
*
* @example
* ```typescript
* try {
* validateInput(schema, input);
* } catch (error) {
* if (error instanceof ZodError) {
* return handleValidationError(error);
* }
* }
* ```
*/
export function handleValidationError(error: ZodError): CallToolResult {
const firstError = error.errors[0];
const fieldPath = firstError?.path.join(".") || "input";
const message = firstError?.message || "Validation failed";
return createErrorResult({
type: "validation",
message: `Validation error: ${fieldPath} - ${message}`,
details: error.errors.map((err) => ({
field: err.path.join("."),
message: err.message,
received: err.code,
})),
statusCode: 400,
});
}
/**
* Map API errors to MCP error result
*
* @param error - Error from API request
* @param context - Additional context about the operation
* @returns MCP tool result with API error details
*
* @example
* ```typescript
* try {
* await hnApi.getItem(itemId);
* } catch (error) {
* return handleAPIError(error, "fetching item");
* }
* ```
*/
export function handleAPIError(error: unknown, context = "API request"): CallToolResult {
if (!(error instanceof Error)) {
return createErrorResult({
type: "unknown",
message: `Unknown error during ${context}`,
details: String(error),
statusCode: 500,
});
}
return handleErrorInstance(error, context);
}
/**
* Handle Error instance and convert to CallToolResult
* Extracted to reduce complexity of handleAPIError
*/
function handleErrorInstance(error: Error, context: string): CallToolResult {
// Check for timeout errors
if (error.message.includes("timeout")) {
return createErrorResult({
type: "network",
message: `Request timeout: ${context} took too long to complete`,
details: error.message,
statusCode: 504,
});
}
// Check for rate limit (HTTP 429)
if (error.message.includes("429")) {
return createErrorResult({
type: "rate_limit",
message: "Rate limit exceeded: 10,000 requests per hour maximum. Please try again later.",
details: error.message,
statusCode: 429,
});
}
// Check for not found (HTTP 404)
if (error.message.includes("404")) {
return createErrorResult({
type: "not_found",
message: `Not found: The requested resource could not be found during ${context}`,
details: error.message,
statusCode: 404,
});
}
// Check for HTTP errors
if (error.message.includes("HTTP")) {
return handleHTTPError(error, context);
}
// Generic API error
return createErrorResult({
type: "api",
message: `API error: Failed during ${context} - ${error.message}`,
details: error.message,
statusCode: 500,
});
}
/**
* Handle HTTP errors and extract status code
*/
function handleHTTPError(error: Error, context: string): CallToolResult {
const statusMatch = error.message.match(/HTTP (\d+)/);
const statusCode = statusMatch?.[1] ? Number.parseInt(statusMatch[1], 10) : 500;
return createErrorResult({
type: "api",
message: `API error: Failed during ${context} - ${error.message}`,
details: error.message,
statusCode,
});
}
/**
* Create a success tool result
*
* @param data - Result data to return
* @returns MCP-compatible tool result with structured content
*
* @example
* ```typescript
* const results = await hnApi.search(params);
* return createSuccessResult(results);
* ```
*/
export function createSuccessResult(data: unknown): CallToolResult {
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
structuredContent: data as Record<string, unknown>,
isError: false,
};
}
/**
* Check if an item exists in API response
*
* @param data - API response data
* @param itemType - Type of item (for error message)
* @param identifier - Item identifier (for error message)
* @returns Error result if item doesn't exist, null otherwise
*/
export function checkItemExists(
data: unknown,
itemType: string,
identifier: string
): CallToolResult | null {
if (!data || (typeof data === "object" && Object.keys(data).length === 0)) {
return createErrorResult({
type: "not_found",
message: `${itemType} not found: No ${itemType.toLowerCase()} with identifier '${identifier}'`,
statusCode: 404,
});
}
return null;
}