feat: add shared API client with auth and error handling
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { getApiBaseUrl, getApiKey, CHARACTER_LIMIT } from "../constants.js";
|
||||
|
||||
/**
|
||||
* Make an authenticated request to the Novu API.
|
||||
*
|
||||
* @param path - API path including version prefix (e.g., "/v1/events/trigger" or "/v2/topics")
|
||||
* @param method - HTTP method
|
||||
* @param data - Request body for POST/PUT/PATCH
|
||||
* @param params - Query parameters for GET requests
|
||||
*/
|
||||
export async function novuRequest<T>(
|
||||
path: string,
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" = "GET",
|
||||
data?: unknown,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
// Strip undefined values from params to avoid sending "?key=undefined"
|
||||
const cleanParams = params
|
||||
? Object.fromEntries(
|
||||
Object.entries(params).filter(([, v]) => v !== undefined)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method,
|
||||
url: `${getApiBaseUrl()}${path}`,
|
||||
data,
|
||||
params: cleanParams,
|
||||
timeout: 30_000,
|
||||
headers: {
|
||||
Authorization: `ApiKey ${getApiKey()}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await axios(config);
|
||||
return response.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an API error into an actionable string for the LLM.
|
||||
*/
|
||||
export function formatApiError(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const body = error.response.data as
|
||||
| { message?: string; error?: string }
|
||||
| undefined;
|
||||
const detail =
|
||||
body?.message || body?.error || JSON.stringify(body);
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
return `Error (400 Bad Request): ${detail}. Check that all required parameters are provided and valid.`;
|
||||
case 401:
|
||||
return "Error (401 Unauthorized): Invalid or missing API key. Set NOVU_SECRET_KEY environment variable with a valid key from https://dashboard.novu.co";
|
||||
case 403:
|
||||
return `Error (403 Forbidden): ${detail}. Check your API key permissions.`;
|
||||
case 404:
|
||||
return `Error (404 Not Found): ${detail}. Verify the resource ID/key is correct.`;
|
||||
case 409:
|
||||
return `Error (409 Conflict): ${detail}. The resource may already exist.`;
|
||||
case 413:
|
||||
return `Error (413 Payload Too Large): ${detail}. Reduce the request body size.`;
|
||||
case 422:
|
||||
return `Error (422 Unprocessable Entity): ${detail}. Check request body schema.`;
|
||||
case 429:
|
||||
return "Error (429 Rate Limited): Too many requests. Wait before retrying. See https://docs.novu.co/api-reference/rate-limiting";
|
||||
default:
|
||||
return `Error (${status}): ${detail}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request timed out after 30s. Try again or check Novu service status.";
|
||||
} else if (error.code === "ENOTFOUND") {
|
||||
return `Error: Cannot reach Novu API at ${getApiBaseUrl()}. Check NOVU_API_URL and network connectivity.`;
|
||||
}
|
||||
}
|
||||
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string if it exceeds CHARACTER_LIMIT, appending a truncation notice.
|
||||
*/
|
||||
export function truncateIfNeeded(text: string): string {
|
||||
if (text.length <= CHARACTER_LIMIT) return text;
|
||||
const truncated = text.slice(0, CHARACTER_LIMIT);
|
||||
return (
|
||||
truncated +
|
||||
"\n\n... [Response truncated. Use pagination or filters to see more results.]"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a standard MCP tool result (success).
|
||||
*/
|
||||
export function toolResult(text: string) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: truncateIfNeeded(text) }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a standard MCP tool result (error).
|
||||
*/
|
||||
export function toolError(error: unknown) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: formatApiError(error) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user