Skip to main content
Glama
api-client.ts.backup19.9 kB
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { AuthToken, ApiResponse, QueryParams, UMBRELLA_ENDPOINTS } from './types.js'; export class UmbrellaApiClient { private baseURL: string; private axiosInstance: AxiosInstance; private _authToken: AuthToken | null = null; private _auth: any = null; // Reference to UmbrellaDualAuth for cloud context switching constructor(baseURL: string) { this.baseURL = baseURL; this.axiosInstance = axios.create({ baseURL: this.baseURL, timeout: 60000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); } setAuthToken(token: AuthToken): void { this._authToken = token; } setAuthHeaders(headers: AuthToken): void { this._authToken = headers; } setAuth(auth: any): void { this._auth = auth; } get authToken(): AuthToken | null { return this._authToken; } private getAuthHeaders(cloudContext?: string, customerAccountKey?: string): Record<string, string> { if (!this._authToken) { throw new Error('Authentication token not set. Please authenticate first.'); } const headers: Record<string, string> = { 'Authorization': this._authToken.Authorization }; // Priority: Customer account key > Cloud context key > Default key let apiKey = null; // 1. Customer-specific account key (highest priority) if (customerAccountKey && this._auth && typeof this._auth.buildCustomerApiKey === 'function') { apiKey = this._auth.buildCustomerApiKey(customerAccountKey); if (apiKey) { console.error(`[API-CLIENT] Using customer account key ${customerAccountKey}: ${apiKey}`); } } // 2. Cloud-specific API key (if no customer key or customer key failed) if (!apiKey && cloudContext && this._auth && typeof this._auth.buildCloudContextApiKey === 'function') { apiKey = this._auth.buildCloudContextApiKey(cloudContext); if (apiKey) { console.error(`[API-CLIENT] Using ${cloudContext.toUpperCase()} cloud context API key: ${apiKey}`); } } // 3. Default API key (fallback) if (!apiKey && this._authToken.apikey) { apiKey = this._authToken.apikey; console.error(`[API-CLIENT] Using default API key: ${apiKey}`); } if (apiKey) { headers['apikey'] = apiKey; } return headers; } async makeRequest(path: string, params?: QueryParams): Promise<ApiResponse> { try { // Special handling for MSP customers endpoint if (path === '/msp/customers') { console.error(`[API-CLIENT] MSP customers endpoint detected - using enhanced method`); return await this.getMspCustomers(); } return await this.makeDirectRequest(path, params); } catch (error: any) { // Error handling is done in makeDirectRequest throw error; } } async makeDirectRequest(path: string, params?: QueryParams): Promise<ApiResponse> { try { // Extract cloud context and customer account key from parameters const cloudContext = params?.cloud_context; const customerAccountKey = params?.customer_account_key; // Convert params to handle arrays properly for axios const axiosParams: Record<string, any> = {}; if (params) { for (const [key, value] of Object.entries(params)) { // Skip internal parameters that shouldn't be sent to API if (key === 'customer_account_key') { continue; } if (Array.isArray(value)) { // Axios handles arrays by repeating the parameter axiosParams[key] = value; } else if (value !== undefined && value !== null) { axiosParams[key] = value; } } } const config: AxiosRequestConfig = { headers: this.getAuthHeaders(cloudContext, customerAccountKey), params: axiosParams }; // DEBUGGING: Log the full request details const fullUrl = `${this.baseURL}${path}`; // Properly handle array parameters and filter parameters const searchParams = new URLSearchParams(); if (params) { for (const [key, value] of Object.entries(params)) { // Handle filter parameters (e.g., filters[service]=AmazonCloudWatch) if (key.startsWith('filters[')) { // Keep the filter format as-is for the API searchParams.append(key, String(value)); } else if (Array.isArray(value)) { // Add each array item as separate parameter value.forEach(item => searchParams.append(key, String(item))); } else if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } } } const queryString = searchParams.toString(); const finalUrl = queryString ? `${fullUrl}?${queryString}` : fullUrl; console.error('\\n🔍 API REQUEST:'); console.error(` URL: ${finalUrl}`); console.error(` Cloud Context: ${cloudContext || 'none'}`); console.error(` Customer Account: ${customerAccountKey || 'none'}`); console.error(` Headers: ${JSON.stringify(this.getAuthHeaders(cloudContext, customerAccountKey), null, 2)}`); console.error(` Params: ${JSON.stringify(params, null, 2)}`); const response = await this.axiosInstance.get(path, config); // DEBUGGING: Log the response details console.error('\\n✅ API RESPONSE:'); console.error(` Status: ${response.status}`); console.error(` Headers: ${JSON.stringify(response.headers, null, 2)}`); console.error(` Data: ${JSON.stringify(response.data, null, 2)}`); console.error(' ====================================='); return { data: response.data, success: true, message: 'Request completed successfully' }; } catch (error: any) { // DEBUGGING: Log the error details console.error('\\n❌ API ERROR:'); if (error.response) { console.error(` Status: ${error.response.status}`); console.error(` Headers: ${JSON.stringify(error.response.headers, null, 2)}`); console.error(` Data: ${JSON.stringify(error.response.data, null, 2)}`); } else if (error.request) { console.error(` Request Error: ${error.message}`); console.error(` URL: ${error.config?.url}`); } else { console.error(` General Error: ${error.message}`); } console.error(' ====================================='); if (error.response) { const status = error.response.status; const message = error.response.data?.message || error.response.statusText || 'Request failed'; if (status === 401) { return { success: false, error: 'Authentication failed. Please check your credentials and try again.', message: message }; } else if (status === 403) { return { success: false, error: 'Access denied. You do not have permission to access this resource.', message: message }; } else if (status === 404) { return { success: false, error: 'Resource not found. Please check the endpoint path.', message: message }; } else if (status === 429) { return { success: false, error: 'Rate limit exceeded. Please wait and try again.', message: message }; } else { return { success: false, error: `Server error (${status}): ${message}`, message: message }; } } else if (error.request) { return { success: false, error: 'Network error: Unable to connect to Umbrella Cost API.', message: 'Please check your network connection and try again.' }; } else { return { success: false, error: `Request error: ${error.message}`, message: error.message }; } } } // Helper methods for common endpoints async getCostAndUsage(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/invoices/caui', params); } async getCosts(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/invoices/caui', params); } async getAccounts(): Promise<ApiResponse> { // DISABLED: This method corrupts session state and breaks recommendations // Returning empty response to prevent session corruption console.error('[GETACCOUNTS] Disabled to prevent session corruption and preserve recommendations'); return { success: false, error: 'getAccounts disabled to prevent session corruption' }; } async getPublicCostAndUsage(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/invoices/cost-and-usage', params); } async getRDSInstanceCosts(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/usage/rds/instance-costs', params); } async getS3BucketCosts(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/usage/s3/bucket-costs', params); } async getResourceExplorerData(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/usage/resource-explorer/resource', params); } async getDashboards(): Promise<ApiResponse> { return this.makeRequest('/dashboards'); } async getBudgets(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/budgets', params); } async getRecommendations(params?: QueryParams & { useNetAmortized?: boolean, cloud_context?: string }): Promise<ApiResponse> { // Use the correct V2 recommendations API with POST method and commonParams header // DEFAULT: Use last 12 months date range to match UI exactly const today = new Date(); const last12Months = new Date(today); last12Months.setFullYear(today.getFullYear() - 1); const fromDate = last12Months.toISOString().split('T')[0]; const toDate = today.toISOString().split('T')[0]; const requestBody = { filters: { user_status: { excluded: false, done: false }, status_filter: "potential_savings", open_recs_creation_date: { from: fromDate, to: toDate } }, sort: [{ by: "savings", order: "desc" }], page_size: 2000, pagination_token: null }; try { // Fetch ALL recommendations through pagination from working V2 API let allRecommendations: any[] = []; let paginationToken: string | null = null; let pageCount = 0; do { const pageRequestBody: any = { ...requestBody, pagination_token: paginationToken }; const response: any = await axios.post('https://api.umbrellacost.io/api/v2/recommendations/list', pageRequestBody, { headers: { ...this.getAuthHeaders(), 'commonParams': '{"isPpApplied":false}', 'Content-Type': 'application/json' }, timeout: 30000 }); // V2 API returns { page: [...], pagination_token: "..." } structure const pageData = response.data.page || []; allRecommendations.push(...pageData); paginationToken = response.data.pagination_token; pageCount++; console.error(`📄 Fetched page ${pageCount}: ${pageData.length} recommendations (token: ${paginationToken ? 'has-next' : 'end'})`); // Safety limit to prevent infinite loops if (pageCount > 20) break; } while (paginationToken); console.error(`📊 Total fetched: ${allRecommendations.length} recommendations across ${pageCount} pages`); // Debug: Log structure of first recommendation to understand field names if (allRecommendations.length > 0) { console.error(`🔍 First recommendation structure:`, JSON.stringify(allRecommendations[0], null, 2)); } const recommendations = allRecommendations; // Filter recommendations based on cloud context (if specified) let filteredRecommendations = recommendations; if (params?.cloud_context) { const targetCloud = params.cloud_context.toUpperCase(); filteredRecommendations = recommendations.filter((rec: any) => { return rec.cloudProvider === targetCloud; }); console.error(`🎯 ${targetCloud} Filtering: ${allRecommendations.length} total -> ${filteredRecommendations.length} ${targetCloud} recommendations`); } else { // No cloud filter specified - return ALL cloud recommendations console.error(`🌐 No cloud filter: Returning all ${allRecommendations.length} recommendations`); } // DEFAULT: Use on-demand (unblended) costs unless client explicitly requests net amortized const useNetAmortized = params?.useNetAmortized === true; const savingsField = useNetAmortized ? 'netAmortized' : 'unblended'; const costField = useNetAmortized ? 'netAmortized' : 'unblended'; const totalSavings = filteredRecommendations.reduce((sum: number, rec: any) => sum + (rec.annualSavings?.[savingsField] || 0), 0); // Calculate category breakdown from filtered recommendations const categoryBreakdown: Record<string, {amount: number, count: number}> = {}; filteredRecommendations.forEach((rec: any) => { const category = rec.category || 'Unknown'; if (!categoryBreakdown[category]) categoryBreakdown[category] = {amount: 0, count: 0}; categoryBreakdown[category].amount += rec.annualSavings?.[savingsField] || 0; categoryBreakdown[category].count += 1; }); const formattedRecommendations = filteredRecommendations.slice(0, 10).map((rec: any) => ({ id: rec.recId, type: rec.typeName || rec.typeId, category: rec.category, service: rec.service, resource_id: rec.resourceId, resource_name: rec.resourceName || rec.resourceId, region: rec.region, annual_savings: rec.annualSavings?.[savingsField] || 0, monthly_savings: rec.monthlySavings?.[savingsField] || 0, annual_current_cost: rec.annualCurrentCost?.[costField] || 0, monthly_current_cost: rec.monthlyCurrentCost?.[costField] || 0, current_instance_type: rec.instanceType, recommended_instance_type: rec.recData?.instance_type_recommended || rec.recData?.recommended_instance_type, recommended_action: rec.recommendedAction, description: rec.recData?.description || `${rec.typeName} recommendation for ${rec.service}`, status: rec.open ? 'Open' : 'Closed', age_days: rec.age, cost_view: useNetAmortized ? 'net_amortized' : 'on_demand_unblended' })); return { success: true, data: formattedRecommendations, total_count: filteredRecommendations.length, total_potential_savings: totalSavings, category_breakdown: categoryBreakdown, message: `Found ${filteredRecommendations.length} cost optimization recommendations with $${totalSavings.toLocaleString()} total potential annual savings` }; } catch (error: any) { console.error('Recommendations API Error:', error.message); if (error.response?.status === 500) { return { success: false, error: 'Recommendations service temporarily unavailable', message: 'The recommendations feature may not be configured for this account' }; } throw error; } } async getAnomalyDetection(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/anomaly-detection', params); } async getCommitmentAnalysis(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/commitment', params); } async getKubernetesCosts(params?: QueryParams): Promise<ApiResponse> { return this.makeRequest('/kubernetes', params); } async getUsers(): Promise<ApiResponse> { return this.makeRequest('/users'); } async getCustomers(): Promise<ApiResponse> { return this.makeRequest('/user-management/customers'); } async getMspCustomers(): Promise<ApiResponse> { console.error(`[API-CLIENT] MSP Customer List: Using auth-based approach directly (pagination API ignores limit parameters)`); // Skip pagination entirely since it doesn't work - API ignores limit/offset parameters // and always returns only 5 customers regardless of pagination settings. // Authentication data shows 300+ accounts available, so use that directly. try { return await this.getMspCustomersFromAuth(); } catch (error: any) { console.error(`[API-CLIENT] MSP Customer List: Auth-based approach failed: ${error.message}`); return { success: false, error: 'Unable to retrieve MSP customer list from authentication data', message: `MSP customer retrieval failed: ${error.message}` }; } } private async getMspCustomersFromAuth(): Promise<ApiResponse> { console.error(`[API-CLIENT] MSP Customer List: Using auth-based approach as fallback`); if (this._auth && typeof this._auth.getAvailableAccounts === 'function') { const availableAccounts = this._auth.getAvailableAccounts(); console.error(`[API-CLIENT] MSP Customer List: Found ${availableAccounts.length} available accounts from auth`); if (availableAccounts.length > 5) { // Convert ALL available accounts to customer format (less filtering for maximum results) const customerAccounts = availableAccounts .filter((acc: any) => { const name = acc.accountName || acc.account_name || acc.name || ''; const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id || ''; // Minimal filtering - only exclude truly invalid entries return name.trim() !== '' && accountKey !== '' && accountKey !== 'undefined'; }) .map((acc: any, index: number) => ({ customerName: acc.accountName || acc.account_name || acc.name || `Account ${acc.accountKey}`, customerCode: `MSP-${acc.accountKey || index}`, customerId: index + 1, linkedAccountIds: [String(acc.accountKey || acc.account_key || acc.accountId || acc.account_id)], customerNameId: acc.accountName || acc.account_name || acc.name || `Account ${acc.accountKey}`, accountKey: acc.accountKey || acc.account_key || acc.accountId || acc.account_id })); console.error(`[API-CLIENT] MSP Customer List: Converted ${availableAccounts.length} auth accounts to ${customerAccounts.length} customer records`); return { data: customerAccounts, success: true, message: `Found ${customerAccounts.length} MSP customers from authentication data (fallback method)` }; } } return { success: false, error: 'Unable to retrieve MSP customer list - pagination and auth fallback both failed', message: 'MSP customer data not available' }; } async getDistinctServiceNames(): Promise<ApiResponse> { return this.makeRequest('/invoices/service-names/distinct'); } async getDistinctServiceCosts(): Promise<ApiResponse> { return this.makeRequest('/invoices/service-costs/distinct'); } // Generic method to get available endpoints getAvailableEndpoints() { return UMBRELLA_ENDPOINTS; } }

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/daviddraiumbrella/invoice-monitoring'

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