Clover MCP Server

by ibraheem4
Verified
/** * Clover API Client * * This file implements a client for interacting with the Clover API. */ import axios, { AxiosInstance } from "axios"; import { OAuthV2Client } from "./oauth-v2.js"; import { logger } from "./logger.js"; // Clover API interfaces export interface CloverItem { id: string; name: string; price: number; priceType: string; defaultTaxRates: boolean; sku?: string; code?: string; unitName?: string; cost?: number; isRevenue?: boolean; stockCount?: number; modifiedTime?: number; } export interface CloverOrder { id: string; currency: string; total: number; state: string; createdTime: number; modifiedTime: number; employee?: { id: string; name: string; }; lineItems?: { id: string; name: string; price: number; quantity: number; }[]; } export interface CloverMerchant { id: string; name: string; address?: { address1?: string; address2?: string; address3?: string; city?: string; state?: string; zip?: string; country?: string; }; phoneNumber?: string; website?: string; currency?: string; } /** * Clover API Client */ export class CloverApiClient { private client!: AxiosInstance; private baseUrl: string; private oauthClient: OAuthV2Client; constructor( clientId: string, clientSecret: string, baseUrl: string, redirectUrl: string ) { this.baseUrl = baseUrl; // Initialize OAuth client this.oauthClient = new OAuthV2Client({ clientId, clientSecret, baseUrl, redirectUrl }); this.updateClient(); } /** * Initialize or update the API client */ private async updateClient() { // Create a base client with minimal configuration const baseClient = axios.create({ baseURL: this.baseUrl, headers: { "Content-Type": "application/json", "Accept": "application/json" } }); // Create a request interceptor to add auth headers baseClient.interceptors.request.use(async (config) => { try { // Try to get a valid token const token = await this.oauthClient.ensureValidToken(); logger.debug(`API Request: ${config.method?.toUpperCase()} ${this.baseUrl}${config.url || ''}`); // Add the auth header config.headers.Authorization = `Bearer ${token}`; return config; } catch (error) { logger.error(`Error getting valid token: ${error}`); return Promise.reject(error); } }); // Handle 401 errors by triggering token refresh baseClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // If the error is 401 and we haven't retried yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { // Try to refresh the token await this.oauthClient.refreshAccessToken(); // Retry the request return baseClient(originalRequest); } catch (refreshError) { logger.error(`Error refreshing token: ${refreshError}`); return Promise.reject(refreshError); } } return Promise.reject(error); } ); this.client = baseClient; } /** * Get the OAuth client */ getOAuthClient() { return this.oauthClient; } /** * Get the base URL */ getBaseUrl(): string { return this.baseUrl; } /** * Get the app ID (client ID) */ async getAppId(): Promise<string | null> { try { // Try to get app ID from token payload const token = await this.oauthClient.ensureValidToken(); if (token.includes(".")) { try { const [, payload] = token.split("."); // Make base64 URL safe by padding if needed const base64 = payload.replace(/-/g, '+').replace(/_/g, '/'); const padding = base64.length % 4; const paddedBase64 = padding ? base64 + '='.repeat(4 - padding) : base64; const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString()); if (decodedPayload.app_uuid) { return decodedPayload.app_uuid; } } catch (e) { // Ignore errors, fall back to client ID } } // Fall back to client ID from config return this.oauthClient.getClientId(); } catch (error) { logger.error(`Error getting app ID: ${error}`); return null; } } /** * Check if we have valid tokens */ hasValidCredentials(): boolean { return this.oauthClient.hasValidTokens(); } /** * Start the OAuth flow to get tokens */ async initiateOAuthFlow(port: number = 4000) { return this.oauthClient.startOAuthFlow(port); } /** * Ensure we have valid credentials before making API calls */ private async ensureCredentials() { const tokens = this.oauthClient.getTokens(); // Check if we have an access token if (!tokens.accessToken) { throw new Error( "API key is required. Use initiate_oauth_flow to obtain it." ); } // Check if we have a merchant ID if (!tokens.merchantId) { throw new Error( "Merchant ID is required. Use initiate_oauth_flow with a valid merchant account." ); } // This will refresh the token if needed await this.oauthClient.ensureValidToken(); } /** * Get merchant information */ async getMerchantInfo(): Promise<CloverMerchant> { await this.ensureCredentials(); try { // Get the access token and merchant ID const token = await this.oauthClient.ensureValidToken(); const merchantId = this.oauthClient.getTokens().merchantId; logger.debug(`Getting merchant info for ID: ${merchantId}`); // Try direct API request const response = await axios.get( `${this.baseUrl}/v3/merchants/${merchantId}`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } } ); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Clover API error: ${error.response?.status} ${error.response?.data?.message || error.message}` ); } throw error; } } /** * List inventory items */ async listInventory( query?: string, offset?: number, limit: number = 100 ): Promise<{ elements: CloverItem[] }> { await this.ensureCredentials(); try { const merchantId = this.oauthClient.getTokens().merchantId; const response = await this.client.get(`/v3/merchants/${merchantId}/items`, { params: { filter: query, offset, limit, }, }); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Clover API error: ${error.response?.status} ${error.response?.data?.message || error.message}` ); } throw error; } } /** * List orders */ async listOrders( filter?: string, start?: string, end?: string, limit: number = 100 ): Promise<{ elements: CloverOrder[] }> { await this.ensureCredentials(); try { const merchantId = this.oauthClient.getTokens().merchantId; const response = await this.client.get(`/v3/merchants/${merchantId}/orders`, { params: { filter, start, end, limit, }, }); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Clover API error: ${error.response?.status} ${error.response?.data?.message || error.message}` ); } throw error; } } /** * Get order by ID */ async getOrder(orderId: string): Promise<CloverOrder> { await this.ensureCredentials(); try { const merchantId = this.oauthClient.getTokens().merchantId; const response = await this.client.get(`/v3/merchants/${merchantId}/orders/${orderId}`); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Clover API error: ${error.response?.status} ${error.response?.data?.message || error.message}` ); } throw error; } } /** * Get item by ID */ async getItem(itemId: string): Promise<CloverItem> { await this.ensureCredentials(); try { const merchantId = this.oauthClient.getTokens().merchantId; const response = await this.client.get(`/v3/merchants/${merchantId}/items/${itemId}`); return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Clover API error: ${error.response?.status} ${error.response?.data?.message || error.message}` ); } throw error; } } }