Skip to main content
Glama

QuickFile MCP Server

by marcusquinn
client.ts6.26 kB
/** * QuickFile API Client * Handles all HTTP communication with QuickFile API * https://api.quickfile.co.uk/ */ import type { QuickFileCredentials, QuickFileRequest, QuickFileResponse, QuickFileError, } from "../types/quickfile.js"; import { loadCredentials, createAuthHeader } from "./auth.js"; // API Configuration const API_BASE_URL = "https://api.quickfile.co.uk"; const API_VERSION = "1_2"; export interface ApiClientOptions { testMode?: boolean; timeout?: number; } // ============================================================================= // Helper Functions (extracted to reduce cognitive complexity) // ============================================================================= function logDebugRequest<TRequest>( url: string, request: QuickFileRequest<TRequest>, ): void { console.error(`[DEBUG] URL: ${url}`); const safeRequest = { payload: { Header: { ...request.payload.Header, Authentication: { AccNumber: "***REDACTED***", MD5Value: "***REDACTED***", ApplicationID: request.payload.Header.Authentication.ApplicationID, }, }, Body: request.payload.Body, }, }; console.error(`[DEBUG] Request: ${JSON.stringify(safeRequest, null, 2)}`); } async function logDebugResponse(response: Response): Promise<void> { const responseText = await response.clone().text(); console.error(`[DEBUG] Response Status: ${response.status}`); console.error(`[DEBUG] Response: ${responseText}`); } function extractResponseBody<TResponse>( data: QuickFileResponse<TResponse>, methodName: string, ): TResponse { const methodResponse = data[methodName]; if (methodResponse && !Array.isArray(methodResponse)) { return (methodResponse as { Body: TResponse }).Body; } // Try to find any response key const responseKey = Object.keys(data).find( (key) => key !== "Errors" && typeof data[key] === "object" && !Array.isArray(data[key]), ); if (responseKey) { const response = data[responseKey] as { Body: TResponse }; return response.Body; } throw new QuickFileApiError("Invalid API response structure", "PARSE_ERROR"); } function handleRequestError(error: unknown, timeout: number): never { if (error instanceof QuickFileApiError) { throw error; } if (error instanceof Error) { if (error.name === "AbortError") { throw new QuickFileApiError( `Request timeout after ${timeout}ms`, "TIMEOUT", ); } throw new QuickFileApiError(error.message, "NETWORK_ERROR"); } throw new QuickFileApiError("Unknown error occurred", "UNKNOWN"); } export class QuickFileApiClient { private readonly credentials: QuickFileCredentials; private testMode: boolean; private readonly timeout: number; constructor(options: ApiClientOptions = {}) { this.credentials = loadCredentials(); this.testMode = options.testMode ?? false; this.timeout = options.timeout ?? 30000; // 30 second default } /** * Make an API request to QuickFile * @param methodName - API method name (e.g., 'Client_Search', 'Invoice_Get') * @param body - Request body parameters * @returns Parsed response body */ async request<TRequest, TResponse>( methodName: string, body: TRequest, ): Promise<TResponse> { const url = this.buildUrl(methodName); const header = createAuthHeader(this.credentials, this.testMode); const request: QuickFileRequest<TRequest> = { payload: { Header: header, Body: body, }, }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { if (process.env.QUICKFILE_DEBUG) { logDebugRequest(url, request); } const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(request), signal: controller.signal, }); clearTimeout(timeoutId); if (process.env.QUICKFILE_DEBUG) { await logDebugResponse(response); } if (!response.ok) { throw new QuickFileApiError( `HTTP ${response.status}: ${response.statusText}`, response.status.toString(), ); } const data = (await response.json()) as QuickFileResponse<TResponse>; if (data.Errors && data.Errors.length > 0) { const errors = data.Errors; throw new QuickFileApiError( errors.map((e: QuickFileError) => e.ErrorMessage).join("; "), errors[0].ErrorCode, ); } return extractResponseBody(data, methodName); } catch (error) { clearTimeout(timeoutId); return handleRequestError(error, this.timeout); } } /** * Build the API URL for a method */ private buildUrl(methodName: string): string { // Convert method name to URL path // e.g., 'System_GetAccountDetails' -> 'system/getaccountdetails' const [category, ...methodParts] = methodName.split("_"); const method = methodParts.join("").toLowerCase(); const path = `${category.toLowerCase()}/${method}`; return `${API_BASE_URL}/${API_VERSION}/${path}`; } /** * Enable/disable test mode */ setTestMode(enabled: boolean): void { this.testMode = enabled; } /** * Get current test mode status */ isTestMode(): boolean { return this.testMode; } /** * Get account number (for display/logging) */ getAccountNumber(): string { return this.credentials.accountNumber; } } /** * Custom error class for QuickFile API errors */ export class QuickFileApiError extends Error { public readonly code: string; constructor(message: string, code: string) { super(message); this.name = "QuickFileApiError"; this.code = code; } } // Singleton instance for convenience let defaultClient: QuickFileApiClient | null = null; /** * Get or create the default API client */ export function getApiClient(options?: ApiClientOptions): QuickFileApiClient { if (!defaultClient || options) { defaultClient = new QuickFileApiClient(options); } return defaultClient; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marcusquinn/quickfile-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server