/**
* Error codes for the MCP server
*/
export const ErrorCode = {
BROWSER_NOT_LAUNCHED: 'E001',
BROWSER_CRASHED: 'E002',
TAB_NOT_FOUND: 'E003',
NO_ACTIVE_TAB: 'E004',
SELECTOR_NOT_FOUND: 'E101',
NAVIGATION_TIMEOUT: 'E102',
NAVIGATION_FAILED: 'E103',
EVALUATION_ERROR: 'E201',
OPERATION_TIMEOUT: 'E301',
VALIDATION_ERROR: 'E401',
UNKNOWN_ERROR: 'E999',
} as const;
/**
* MCP error structure
*/
export interface McpError {
/** Error code */
code: string;
/** Human-readable error message */
message: string;
/** Whether the error is recoverable */
recoverable: boolean;
}
/**
* Success result
*/
export interface Success<T> {
success: true;
data: T;
}
/**
* Failure result
*/
export interface Failure<E = McpError> {
success: false;
error: E;
}
/**
* Result type for operations that can fail
*/
export type Result<T, E = McpError> = Success<T> | Failure<E>;
/**
* Create a success result
*/
export function ok<T>(data: T): Success<T> {
return { success: true, data };
}
/**
* Create a failure result
*/
export function err<E = McpError>(error: E): Failure<E> {
return { success: false, error };
}
/**
* Create a browser not launched error
*/
export function browserNotLaunched(): McpError {
return {
code: ErrorCode.BROWSER_NOT_LAUNCHED,
message: 'Browser has not been launched',
recoverable: true,
};
}
/**
* Create a browser crashed error
*/
export function browserCrashed(reason?: string): McpError {
return {
code: ErrorCode.BROWSER_CRASHED,
message: reason ? `Browser crashed: ${reason}` : 'Browser crashed unexpectedly',
recoverable: true,
};
}
/**
* Create a tab not found error
*/
export function tabNotFound(tabId: string): McpError {
return {
code: ErrorCode.TAB_NOT_FOUND,
message: `Tab not found: ${tabId}`,
recoverable: false,
};
}
/**
* Create a no active tab error
*/
export function noActiveTab(): McpError {
return {
code: ErrorCode.NO_ACTIVE_TAB,
message: 'No active tab available',
recoverable: true,
};
}
/**
* Create a selector not found error
*/
export function selectorNotFound(selector: string): McpError {
return {
code: ErrorCode.SELECTOR_NOT_FOUND,
message: `Element not found for selector: ${selector}`,
recoverable: false,
};
}
/**
* Create a navigation timeout error
*/
export function navigationTimeout(url: string, timeoutMs?: number): McpError {
const timeoutInfo = timeoutMs ? ` after ${timeoutMs}ms` : '';
return {
code: ErrorCode.NAVIGATION_TIMEOUT,
message: `Navigation to ${url} timed out${timeoutInfo}`,
recoverable: true,
};
}
/**
* Create a navigation failed error
*/
export function navigationFailed(url: string, reason: string): McpError {
return {
code: ErrorCode.NAVIGATION_FAILED,
message: `Navigation to ${url} failed: ${reason}`,
recoverable: false,
};
}
/**
* Create an evaluation error
*/
export function evaluationError(message: string): McpError {
return {
code: ErrorCode.EVALUATION_ERROR,
message: `JavaScript evaluation failed: ${message}`,
recoverable: false,
};
}
/**
* Create an operation timeout error
*/
export function operationTimeout(operation: string, timeoutMs?: number): McpError {
const timeoutInfo = timeoutMs ? ` after ${timeoutMs}ms` : '';
return {
code: ErrorCode.OPERATION_TIMEOUT,
message: `Operation "${operation}" timed out${timeoutInfo}`,
recoverable: true,
};
}
/**
* Create a validation error
*/
export function validationError(message: string): McpError {
return {
code: ErrorCode.VALIDATION_ERROR,
message: `Validation error: ${message}`,
recoverable: false,
};
}
/**
* Create an unknown error
*/
export function unknownError(message: string): McpError {
return {
code: ErrorCode.UNKNOWN_ERROR,
message: message,
recoverable: false,
};
}
/**
* Convert an unknown error to McpError
*/
export function normalizeError(error: unknown): McpError {
if (isError(error)) {
const message = error.message.toLowerCase();
// Check for specific Puppeteer errors
if (message.includes('navigation timeout') || message.includes('timeout') && message.includes('navigation')) {
return navigationTimeout(extractUrl(error.message) ?? 'unknown');
}
if (message.includes('waiting for selector') || message.includes('failed to find element') || message.includes('no element found')) {
return selectorNotFound(extractSelector(error.message) ?? 'unknown');
}
if (message.includes('timeout') && (message.includes('selector') || message.includes('element'))) {
return operationTimeout('wait_for_selector');
}
if (message.includes('target closed') || message.includes('session closed')) {
return browserCrashed('Target closed');
}
if (message.includes('protocol error')) {
return browserCrashed(error.message);
}
return unknownError(error.message);
}
return unknownError(String(error));
}
/**
* Type guard for Error
*/
function isError(error: unknown): error is Error {
return error instanceof Error;
}
/**
* Extract URL from error message
*/
function extractUrl(message: string): string | undefined {
const match = message.match(/https?:\/\/[^\s]+/);
return match?.[0];
}
/**
* Extract selector from error message
*/
function extractSelector(message: string): string | undefined {
const match = message.match(/selector ['"`]([^'"`]+)['"`]/);
return match?.[1];
}
/**
* Format an MCP error for tool response
*/
export function formatErrorResponse(error: McpError): { isError: true; content: Array<{ type: 'text'; text: string }> } {
return {
isError: true,
content: [{
type: 'text',
text: JSON.stringify({
error: error.code,
message: error.message,
recoverable: error.recoverable,
}),
}],
};
}
/**
* Format a success response for tool
*/
export function formatSuccessResponse<T>(data: T): { content: Array<{ type: 'text'; text: string }> } {
return {
content: [{
type: 'text',
text: JSON.stringify(data),
}],
};
}
/**
* Handle a result and format it for tool response
*/
export function handleResult<T>(result: Result<T>): { isError?: boolean; content: Array<{ type: 'text'; text: string }> } {
if (result.success) {
return formatSuccessResponse(result.data);
}
return formatErrorResponse(result.error);
}