roam-research-mcp
Version:
MCP server and CLI for Roam Research
132 lines (131 loc) • 4.08 kB
JavaScript
/**
* 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;
}