/**
* Error codes for MCP tool responses.
*/
export enum ErrorCode {
UNAUTHORIZED = 'UNAUTHORIZED',
NOT_FOUND = 'NOT_FOUND',
RATE_LIMITED = 'RATE_LIMITED',
INVALID_INPUT = 'INVALID_INPUT',
API_ERROR = 'API_ERROR',
TECH_NOT_FOUND = 'TECH_NOT_FOUND',
TIMEOUT = 'TIMEOUT',
CONFIG_ERROR = 'CONFIG_ERROR'
}
/**
* MCP-friendly error with code and optional suggestions.
*/
export class McpError extends Error {
code: ErrorCode;
suggestions?: string[];
constructor(code: ErrorCode, message: string, suggestions?: string[]) {
super(message);
this.name = 'McpError';
this.code = code;
this.suggestions = suggestions;
}
/**
* Format error for MCP response text.
*/
toResponseText(): string {
let text = `**Error (${this.code})**: ${this.message}`;
if (this.suggestions && this.suggestions.length > 0) {
text += `\n\n**Suggestions**:\n${this.suggestions.map((s) => `- ${s}`).join('\n')}`;
}
return text;
}
}
/**
* Calculate Levenshtein distance between two strings.
*/
export function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
);
}
}
}
return matrix[b.length][a.length];
}
/**
* Find similar strings using Levenshtein distance.
*/
export function findSimilar(input: string, candidates: string[], limit = 3): string[] {
const inputLower = input.toLowerCase();
return candidates
.map((candidate) => ({
candidate,
distance: levenshteinDistance(inputLower, candidate.toLowerCase())
}))
.filter((item) => item.distance <= 3) // Max 3 edits
.sort((a, b) => a.distance - b.distance)
.slice(0, limit)
.map((item) => item.candidate);
}
/**
* Create error for unknown technology ID.
*/
export function techNotFoundError(techId: string, availableTechs: string[]): McpError {
const similar = findSimilar(techId, availableTechs);
const suggestions =
similar.length > 0
? [`Did you mean: ${similar.join(', ')}?`, 'Use list_technologies to see all available IDs.']
: ['Use list_technologies to see all available technology IDs.'];
return new McpError(ErrorCode.TECH_NOT_FOUND, `Unknown technology: "${techId}"`, suggestions);
}
/**
* Map HTTP status codes to error codes.
*/
export function httpStatusToErrorCode(status: number): ErrorCode {
switch (status) {
case 401:
case 403:
return ErrorCode.UNAUTHORIZED;
case 404:
return ErrorCode.NOT_FOUND;
case 429:
return ErrorCode.RATE_LIMITED;
case 400:
case 422:
return ErrorCode.INVALID_INPUT;
default:
return ErrorCode.API_ERROR;
}
}
/**
* Create error from HTTP response.
*/
export function apiError(status: number, message?: string): McpError {
const code = httpStatusToErrorCode(status);
const defaultMessages: Record<ErrorCode, string> = {
[ErrorCode.UNAUTHORIZED]: 'API key is invalid or missing. Set STACKSFINDER_API_KEY.',
[ErrorCode.NOT_FOUND]: 'Resource not found.',
[ErrorCode.RATE_LIMITED]: 'Rate limit exceeded. Please try again later.',
[ErrorCode.INVALID_INPUT]: 'Invalid request parameters.',
[ErrorCode.API_ERROR]: `API request failed with status ${status}.`,
[ErrorCode.TECH_NOT_FOUND]: 'Technology not found.',
[ErrorCode.TIMEOUT]: 'Request timed out.',
[ErrorCode.CONFIG_ERROR]: 'Configuration error.'
};
return new McpError(code, message || defaultMessages[code]);
}