UNPKG

qbo-mcp-ts

Version:

TypeScript QuickBooks Online MCP Server with enhanced features and dual transport support

334 lines 13.4 kB
"use strict"; /** * QuickBooks Online API Client with retry logic and error handling */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QBOApiClient = void 0; const axios_1 = __importDefault(require("axios")); const axios_retry_1 = __importDefault(require("axios-retry")); const types_1 = require("../types"); const config_1 = require("../utils/config"); const logger_1 = require("../utils/logger"); /** * QuickBooks Online API Client */ class QBOApiClient { axiosInstance; tokens; baseUrl; authUrl; qboConfig; tokenRefreshPromise; constructor(qboConfig) { this.qboConfig = qboConfig || config_1.config.getQBOConfig(); const apiConfig = config_1.config.getAPIConfig(); // Set base URLs based on environment if (this.qboConfig.environment === 'production') { this.baseUrl = `https://quickbooks.api.intuit.com/v3/company/${this.qboConfig.companyId}`; this.authUrl = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'; } else { this.baseUrl = `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.qboConfig.companyId}`; this.authUrl = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'; } // Initialize tokens this.tokens = { accessToken: '', refreshToken: this.qboConfig.refreshToken, expiresAt: new Date(0), // Force initial refresh }; // Create axios instance this.axiosInstance = axios_1.default.create({ baseURL: this.baseUrl, timeout: apiConfig.timeout, headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); // Add request interceptor for authentication this.axiosInstance.interceptors.request.use(async (config) => { // Ensure we have a valid access token await this.ensureValidToken(); // Add authorization header config.headers.Authorization = `Bearer ${this.tokens.accessToken}`; // Log API request const timer = logger_1.logger.startTimer(); config.metadata = { timer }; logger_1.logger.api('Request', { method: config.method?.toUpperCase(), endpoint: config.url, }); return config; }, (error) => { logger_1.logger.error('Request interceptor error', error); return Promise.reject(error); }); // Add response interceptor for logging and error handling this.axiosInstance.interceptors.response.use((response) => { // Log successful response const duration = response.config.metadata?.timer?.() || 0; logger_1.logger.api('Response', { method: response.config.method?.toUpperCase(), endpoint: response.config.url, statusCode: response.status, duration, }); return response; }, async (error) => { const duration = error.config?.metadata?.timer?.() || 0; // Log error response logger_1.logger.api('Error', { method: error.config?.method?.toUpperCase(), endpoint: error.config?.url, statusCode: error.response?.status, duration, error: error.message, }); // Handle specific error types if (error.response) { const status = error.response.status; const data = error.response.data; switch (status) { case 401: // Try to refresh token once if (!error.config?.retry) { error.config.retry = true; await this.refreshAccessToken(); return this.axiosInstance.request(error.config); } throw new types_1.AuthenticationError('Authentication failed. Please check your credentials.', data); case 429: const retryAfter = error.response.headers['retry-after']; throw new types_1.RateLimitError('Rate limit exceeded. Please try again later.', retryAfter ? parseInt(retryAfter) : undefined); case 400: throw new types_1.QBOError(data?.Fault?.Error?.[0]?.Message || 'Bad request', 'VALIDATION_ERROR', 400, data); case 403: throw new types_1.QBOError('Access forbidden. Check your permissions.', 'FORBIDDEN', 403, data); case 404: throw new types_1.QBOError('Resource not found', 'NOT_FOUND', 404, data); case 500: case 502: case 503: case 504: throw new types_1.NetworkError('QuickBooks service is temporarily unavailable', data); default: throw new types_1.QBOError(data?.Fault?.Error?.[0]?.Message || 'An error occurred', 'UNKNOWN_ERROR', status, data); } } else if (error.request) { throw new types_1.NetworkError('Network error. Please check your connection.'); } else { throw new types_1.QBOError(error.message, 'REQUEST_ERROR'); } }); // Configure retry logic if enabled if (apiConfig.enableRetry) { (0, axios_retry_1.default)(this.axiosInstance, { retries: apiConfig.retryAttempts, retryDelay: (retryCount) => { const delay = apiConfig.retryDelay * Math.pow(2, retryCount - 1); logger_1.logger.info(`Retrying request (attempt ${retryCount}), waiting ${delay}ms`); return delay; }, retryCondition: (error) => { // Retry on network errors and 5xx status codes return (axios_retry_1.default.isNetworkOrIdempotentRequestError(error) || (error.response?.status ? error.response.status >= 500 : false)); }, onRetry: (retryCount, error) => { logger_1.logger.warn(`Retry attempt ${retryCount} for ${error.config?.url}`, { error: error.message, }); }, }); } } /** * Ensure we have a valid access token */ async ensureValidToken() { const now = new Date(); const expiryBuffer = new Date(this.tokens.expiresAt.getTime() - 60000); // 1 minute buffer if (now >= expiryBuffer) { // Use existing refresh promise if one is in progress if (!this.tokenRefreshPromise) { this.tokenRefreshPromise = this.refreshAccessToken(); } await this.tokenRefreshPromise; this.tokenRefreshPromise = undefined; } } /** * Refresh the OAuth2 access token */ async refreshAccessToken() { logger_1.logger.info('Refreshing QuickBooks access token'); try { const authHeader = Buffer.from(`${this.qboConfig.clientId}:${this.qboConfig.clientSecret}`).toString('base64'); const response = await axios_1.default.post(this.authUrl, new URLSearchParams({ grant_type: 'refresh_token', refresh_token: this.tokens.refreshToken, }), { headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${authHeader}`, }, }); const data = response.data; // Update tokens this.tokens = { accessToken: data.access_token, refreshToken: data.refresh_token || this.tokens.refreshToken, expiresAt: new Date(Date.now() + data.expires_in * 1000), }; logger_1.logger.info('Successfully refreshed QuickBooks access token', { expiresAt: this.tokens.expiresAt.toISOString(), }); } catch (error) { logger_1.logger.error('Failed to refresh access token', error); throw new types_1.AuthenticationError('Failed to refresh access token. Please check your credentials.'); } } /** * Make a GET request to QuickBooks API */ async get(endpoint, params) { const response = await this.axiosInstance.get(endpoint, { params }); return response.data; } /** * Make a POST request to QuickBooks API */ async post(endpoint, data) { const response = await this.axiosInstance.post(endpoint, data); return response.data; } /** * Make a PUT request to QuickBooks API */ async put(endpoint, data) { const response = await this.axiosInstance.put(endpoint, data); return response.data; } /** * Make a DELETE request to QuickBooks API */ async delete(endpoint) { const response = await this.axiosInstance.delete(endpoint); return response.data; } /** * Execute a query using QuickBooks Query Language */ async query(query) { const response = await this.axiosInstance.get('/query', { params: { query }, }); return response.data; } /** * Send an email for an entity (invoice, estimate, etc.) */ async sendEmail(entityType, entityId, email) { const endpoint = `/${entityType.toLowerCase()}/${entityId}/send`; const params = email ? { sendTo: email } : undefined; const response = await this.axiosInstance.post(endpoint, null, { params }); return response.data; } /** * Download a PDF for an entity */ async downloadPDF(entityType, entityId) { const endpoint = `/${entityType.toLowerCase()}/${entityId}/pdf`; const response = await this.axiosInstance.get(endpoint, { responseType: 'arraybuffer', headers: { Accept: 'application/pdf', }, }); return Buffer.from(response.data); } /** * Get company info */ async getCompanyInfo() { return this.get('/companyinfo/1'); } /** * Get current user info */ async getCurrentUser() { // const authHeader = Buffer.from( // `${this.qboConfig.clientId}:${this.qboConfig.clientSecret}` // ).toString('base64'); const response = await axios_1.default.get('https://accounts.platform.intuit.com/v1/openid_connect/userinfo', { headers: { Accept: 'application/json', Authorization: `Bearer ${this.tokens.accessToken}`, }, }); return response.data; } /** * Batch operations */ async batch(operations) { const batchRequest = { BatchItemRequest: operations.map((op) => { const item = { bId: op.bId }; switch (op.operation) { case 'create': item[op.entity] = op.data; item.operation = 'create'; break; case 'update': item[op.entity] = op.data; item.operation = 'update'; break; case 'delete': item[op.entity] = { Id: op.data.Id }; item.operation = 'delete'; break; case 'query': item.Query = op.query; break; } return item; }), }; return this.post('/batch', batchRequest); } /** * Get API limits and usage */ async getApiLimits() { // QuickBooks doesn't provide a direct API for this, // but we can track it from response headers try { const response = await this.axiosInstance.get('/companyinfo/1'); const remaining = parseInt(response.headers['x-ratelimit-remaining'] || '1000'); const limit = parseInt(response.headers['x-ratelimit-limit'] || '1000'); const reset = response.headers['x-ratelimit-reset'] ? new Date(parseInt(response.headers['x-ratelimit-reset']) * 1000) : new Date(Date.now() + 3600000); return { remaining, limit, reset }; } catch (_error) { // Return default values if headers are not available return { remaining: 1000, limit: 1000, reset: new Date(Date.now() + 3600000), }; } } } exports.QBOApiClient = QBOApiClient; //# sourceMappingURL=client.js.map