// ============================================================================ // CloseBot API HTTP Client // ============================================================================ const BASE_URL = "https://api.closebot.com"; export class CloseBotClient { private apiKey: string; private baseUrl: string; constructor(apiKey?: string, baseUrl?: string) { this.apiKey = apiKey || process.env.CLOSEBOT_API_KEY || ""; this.baseUrl = baseUrl || process.env.CLOSEBOT_BASE_URL || BASE_URL; if (!this.apiKey) { throw new Error( "CloseBot API key is required. Set CLOSEBOT_API_KEY environment variable." ); } } private buildUrl(path: string, query?: Record): string { const url = new URL(path, this.baseUrl); if (query) { for (const [key, value] of Object.entries(query)) { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, String(value)); } } } return url.toString(); } private get headers(): Record { return { "X-CB-KEY": this.apiKey, "Content-Type": "application/json", Accept: "application/json", }; } async get( path: string, query?: Record ): Promise { const url = this.buildUrl(path, query); const response = await fetch(url, { method: "GET", headers: this.headers, }); return this.handleResponse(response); } async post( path: string, body?: unknown, query?: Record ): Promise { const url = this.buildUrl(path, query); const options: RequestInit = { method: "POST", headers: this.headers, }; if (body !== undefined) { options.body = JSON.stringify(body); } const response = await fetch(url, options); return this.handleResponse(response); } async put( path: string, body?: unknown, query?: Record ): Promise { const url = this.buildUrl(path, query); const options: RequestInit = { method: "PUT", headers: this.headers, }; if (body !== undefined) { options.body = JSON.stringify(body); } const response = await fetch(url, options); return this.handleResponse(response); } async delete( path: string, query?: Record ): Promise { const url = this.buildUrl(path, query); const response = await fetch(url, { method: "DELETE", headers: this.headers, }); return this.handleResponse(response); } async postFormData( path: string, formData: FormData, query?: Record ): Promise { const url = this.buildUrl(path, query); const headers: Record = { "X-CB-KEY": this.apiKey, Accept: "application/json", }; const response = await fetch(url, { method: "POST", headers, body: formData, }); return this.handleResponse(response); } async putFormData( path: string, formData: FormData, query?: Record ): Promise { const url = this.buildUrl(path, query); const headers: Record = { "X-CB-KEY": this.apiKey, Accept: "application/json", }; const response = await fetch(url, { method: "PUT", headers, body: formData, }); return this.handleResponse(response); } private async handleResponse(response: Response): Promise { if (!response.ok) { let errorBody: string; try { errorBody = await response.text(); } catch { errorBody = "Unable to read error body"; } throw new ApiError( `CloseBot API error ${response.status}: ${response.statusText}`, response.status, errorBody ); } const contentType = response.headers.get("content-type"); if (!contentType || response.status === 204) { return {} as T; } if (contentType.includes("application/json") || contentType.includes("text/json")) { return (await response.json()) as T; } const text = await response.text(); try { return JSON.parse(text) as T; } catch { return text as unknown as T; } } } export class ApiError extends Error { public statusCode: number; public responseBody: string; constructor(message: string, statusCode: number, responseBody: string) { super(message); this.name = "ApiError"; this.statusCode = statusCode; this.responseBody = responseBody; } } /** Format API result as MCP text content */ export function ok(data: unknown): { content: Array<{ type: "text"; text: string }>; } { return { content: [ { type: "text" as const, text: typeof data === "string" ? data : JSON.stringify(data, null, 2), }, ], }; } /** Format error as MCP error content */ export function err(error: unknown): { content: Array<{ type: "text"; text: string }>; isError: boolean; } { const message = error instanceof ApiError ? `API Error ${error.statusCode}: ${error.message}\n${error.responseBody}` : error instanceof Error ? error.message : String(error); return { content: [{ type: "text" as const, text: message }], isError: true, }; }