UNPKG

roam-research-mcp

Version:
132 lines (131 loc) • 4.08 kB
/** * Structured error types for the Roam MCP server. * Provides consistent error handling across all tools. */ /** * An error carrying a machine-readable code and arbitrary recovery context. * * The context matters more than it looks: keys are spread into the error body * the agent receives, so a failure can ship the facts needed to fix it — * `available_graphs` on an unknown graph, a page title on a miss — instead of a * prose sentence the agent has to parse and usually can't act on. */ export class RoamError extends Error { constructor(message, code = 'API_ERROR', context) { super(message); this.code = code; this.context = context; this.name = 'RoamError'; // Extending a builtin breaks instanceof under some transpile targets. Object.setPrototypeOf(this, RoamError.prototype); } } /** * Render an error as an MCP tool result. * * Returned rather than thrown: MCP treats a thrown error as a protocol failure, * while `isError: true` with content is a *tool* failure the model can read and * act on. Context keys are spread alongside code and message. */ export function toErrorResult(error) { const isRoam = error instanceof RoamError; const body = { error: { code: isRoam ? error.code : inferCode(error), message: error instanceof Error ? error.message : String(error), ...(isRoam ? error.context ?? {} : {}), }, }; return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], isError: true, }; } /** Best-effort code for errors that were not raised as a RoamError. */ function inferCode(error) { if (isRateLimitError(error)) return 'RATE_LIMIT'; if (isNetworkError(error)) return 'NETWORK_ERROR'; return 'API_ERROR'; } /** * Creates a structured validation error response. */ export function createValidationError(message, details, recovery) { return { code: 'VALIDATION_ERROR', message, details, recovery }; } /** * Creates a structured rate limit error response. */ export function createRateLimitError(retryAfterMs) { return { code: 'RATE_LIMIT', message: 'Too many requests, please retry after backoff', recovery: { retry_after_ms: retryAfterMs ?? 60000, suggestion: 'Wait for the specified duration before retrying' } }; } /** * Creates a structured API error response. */ export function createApiError(message, details) { return { code: 'API_ERROR', message, details }; } /** * Creates a structured transaction failed error response. */ export function createTransactionFailedError(message, failedAtAction, committed) { return { success: false, error: { code: 'TRANSACTION_FAILED', message, details: failedAtAction !== undefined ? { action_index: failedAtAction } : undefined }, committed }; } /** * Checks if an error is a rate limit error based on error message. */ export function isRateLimitError(error) { if (error instanceof Error) { const message = error.message.toLowerCase(); return message.includes('too many requests') || message.includes('rate limit') || message.includes('try again in'); } if (typeof error === 'string') { const message = error.toLowerCase(); return message.includes('too many requests') || message.includes('rate limit') || message.includes('try again in'); } return false; } /** * Checks if an error is a network error. */ export function isNetworkError(error) { if (error instanceof Error) { const message = error.message.toLowerCase(); return message.includes('network') || message.includes('econnrefused') || message.includes('econnreset') || message.includes('etimedout') || message.includes('socket'); } return false; }