#!/usr/bin/env node
import dotenv from 'dotenv';
import { UmbrellaAuth } from './auth.js';
import { UmbrellaApiClient } from './api-client.js';
import { getTestCredentialSets } from './auth-helper.js';
import { UMBRELLA_ENDPOINTS } from './types.js';
dotenv.config();
interface AccountInfo {
accountId: string;
accountName?: string;
cloudType: 'AWS' | 'Azure' | 'GCP';
organizationId?: string;
isDefault?: boolean;
region?: string;
}
interface OrganizationInfo {
organizationId: string;
organizationName?: string;
accounts: AccountInfo[];
cloudType?: string;
}
interface DiscoveredContext {
organizations: OrganizationInfo[];
allAccounts: AccountInfo[];
awsAccounts: AccountInfo[];
azureAccounts: AccountInfo[];
gcpAccounts: AccountInfo[];
defaultAccountByCloud: Record<string, AccountInfo>;
}
interface EnhancedTestResult {
endpoint: string;
method: string;
description: string;
category: string;
success: boolean;
error?: string;
data?: any;
responseTime: number;
accountContext?: AccountInfo;
organizationContext?: OrganizationInfo;
providedParams?: Record<string, any>;
}
class AccountDiscoveryTester {
private auth: UmbrellaAuth;
private apiClient: UmbrellaApiClient;
private baseURL: string;
private credentials: { username: string; password: string } | null = null;
private discoveredContext: DiscoveredContext | null = null;
constructor(baseURL: string) {
this.baseURL = baseURL;
this.auth = new UmbrellaAuth(baseURL);
this.apiClient = new UmbrellaApiClient(baseURL);
}
async authenticate(credentials: { username: string; password: string }): Promise<boolean> {
try {
const authResult = await this.auth.authenticate(credentials);
const authHeaders = this.auth.getAuthHeaders();
this.apiClient.setAuthToken(authHeaders);
this.credentials = credentials;
return true;
} catch (error: any) {
console.error(`β Authentication failed for ${credentials.username}: ${error.message}`);
return false;
}
}
async discoverAccountContext(): Promise<DiscoveredContext> {
console.log('\nπ DISCOVERING ACCOUNT & ORGANIZATION CONTEXT');
console.log('='.repeat(60));
const context: DiscoveredContext = {
organizations: [],
allAccounts: [],
awsAccounts: [],
azureAccounts: [],
gcpAccounts: [],
defaultAccountByCloud: {}
};
// Try multiple discovery endpoints based on frontend analysis
const discoveryEndpoints = [
// User management endpoints
{ path: '/user-management/accounts', description: 'User accounts' },
{ path: '/user-management/organization', description: 'Organization info' },
{ path: '/user-management/customers', description: 'Customer accounts' },
// Account-specific endpoints
{ path: '/users/linked-accounts', description: 'Linked accounts' },
{ path: '/users/cost-centers', description: 'Cost centers' },
{ path: '/admin/accounts', description: 'Admin accounts view' },
// Division/customer endpoints
{ path: '/divisions', description: 'Account divisions' },
{ path: '/divisions/customers', description: 'Division customers' },
// Public/internal endpoints that might reveal account info
{ path: '/invoices/internal/accounts', description: 'Internal accounts' },
{ path: '/usage/accounts', description: 'Usage accounts' },
];
for (const endpoint of discoveryEndpoints) {
try {
console.log(` π Trying ${endpoint.path}...`);
const response = await this.apiClient.makeRequest(endpoint.path, {});
if (response.success && response.data) {
console.log(` β
${endpoint.description}: Found data`);
// Parse account information from response
const accounts = this.parseAccountsFromResponse(response.data, endpoint.path);
context.allAccounts.push(...accounts);
console.log(` π Extracted ${accounts.length} accounts`);
// Show sample account if available
if (accounts.length > 0) {
const sample = accounts[0];
console.log(` π Sample: ${sample.accountId} (${sample.cloudType})`);
}
} else {
console.log(` β οΈ ${endpoint.description}: ${response.error || 'No data'}`);
}
} catch (error: any) {
console.log(` β ${endpoint.description}: ${error.message}`);
}
// Brief pause between requests
await new Promise(resolve => setTimeout(resolve, 200));
}
// Try to discover from working endpoints we know have account context
await this.discoverFromWorkingEndpoints(context);
// Process and categorize discovered accounts
this.categorizeAccounts(context);
// Try to determine default accounts per cloud
this.determineDefaultAccounts(context);
this.discoveredContext = context;
return context;
}
private async discoverFromWorkingEndpoints(context: DiscoveredContext): Promise<void> {
console.log('\n π Discovering from known working endpoints...');
// Try recommendations report - it often contains account/org info
try {
const response = await this.apiClient.makeRequest('/recommendations/report', {});
if (response.success && response.data) {
console.log(' π Analyzing recommendations report for account context...');
// Look for account/organization IDs in the response
const accounts = this.extractAccountsFromRecommendations(response.data);
context.allAccounts.push(...accounts);
if (accounts.length > 0) {
console.log(` β
Found ${accounts.length} accounts from recommendations`);
}
}
} catch (error) {
console.log(' β οΈ Could not analyze recommendations for accounts');
}
// Try service names - sometimes contains account-specific data
try {
const response = await this.apiClient.makeRequest('/invoices/service-names/distinct', { limit: 5 });
if (response.success) {
console.log(' β
Service names endpoint confirmed working');
}
} catch (error) {
console.log(' β οΈ Service names endpoint failed');
}
}
private parseAccountsFromResponse(data: any, endpointPath: string): AccountInfo[] {
const accounts: AccountInfo[] = [];
try {
// Handle different response structures based on endpoint
if (Array.isArray(data)) {
data.forEach((item: any) => {
const account = this.extractAccountFromItem(item, endpointPath);
if (account) accounts.push(account);
});
} else if (typeof data === 'object' && data !== null) {
// Single account object
const account = this.extractAccountFromItem(data, endpointPath);
if (account) accounts.push(account);
// Check for nested account arrays
Object.keys(data).forEach(key => {
if (Array.isArray(data[key]) && key.toLowerCase().includes('account')) {
data[key].forEach((item: any) => {
const account = this.extractAccountFromItem(item, endpointPath);
if (account) accounts.push(account);
});
}
});
}
} catch (error) {
console.log(` β οΈ Error parsing accounts from ${endpointPath}: ${error}`);
}
return accounts;
}
private extractAccountFromItem(item: any, source: string): AccountInfo | null {
if (!item || typeof item !== 'object') return null;
// Common account ID field patterns
const accountIdFields = ['accountId', 'account_id', 'awsAccountId', 'aws_account_id', 'id'];
const accountNameFields = ['accountName', 'account_name', 'name', 'displayName'];
const cloudTypeFields = ['cloudType', 'cloud_type', 'provider', 'cloudProvider'];
const orgIdFields = ['organizationId', 'organization_id', 'orgId', 'org_id'];
let accountId = '';
let accountName = '';
let cloudType: 'AWS' | 'Azure' | 'GCP' = 'AWS'; // Default to AWS
let organizationId = '';
// Extract account ID
for (const field of accountIdFields) {
if (item[field]) {
accountId = String(item[field]);
break;
}
}
// Extract account name
for (const field of accountNameFields) {
if (item[field]) {
accountName = String(item[field]);
break;
}
}
// Extract cloud type
for (const field of cloudTypeFields) {
if (item[field]) {
const type = String(item[field]).toUpperCase();
if (type.includes('AWS') || type.includes('AMAZON')) cloudType = 'AWS';
else if (type.includes('AZURE') || type.includes('MICROSOFT')) cloudType = 'Azure';
else if (type.includes('GCP') || type.includes('GOOGLE')) cloudType = 'GCP';
break;
}
}
// Extract organization ID
for (const field of orgIdFields) {
if (item[field]) {
organizationId = String(item[field]);
break;
}
}
// Must have at least an account ID to be valid
if (!accountId) return null;
return {
accountId,
accountName: accountName || undefined,
cloudType,
organizationId: organizationId || undefined,
isDefault: false // Will be determined later
};
}
private extractAccountsFromRecommendations(data: any): AccountInfo[] {
const accounts: AccountInfo[] = [];
try {
if (Array.isArray(data)) {
data.forEach(report => {
if (report.accountId) {
accounts.push({
accountId: report.accountId,
accountName: report.accountName || undefined,
cloudType: 'AWS', // Recommendations are typically AWS-focused
organizationId: report.organizationId || undefined,
isDefault: true // Recommendations usually show primary accounts
});
}
// Check for linked accounts
if (report.linkedAccountIds && Array.isArray(report.linkedAccountIds)) {
report.linkedAccountIds.forEach((accountId: string) => {
accounts.push({
accountId,
cloudType: 'AWS',
organizationId: report.organizationId || undefined,
isDefault: false
});
});
}
});
}
} catch (error) {
console.log(` β οΈ Error extracting accounts from recommendations: ${error}`);
}
return accounts;
}
private categorizeAccounts(context: DiscoveredContext): void {
// Remove duplicates based on accountId
const uniqueAccounts = context.allAccounts.reduce((acc: AccountInfo[], current) => {
const exists = acc.find(account => account.accountId === current.accountId);
if (!exists) {
acc.push(current);
}
return acc;
}, []);
context.allAccounts = uniqueAccounts;
// Categorize by cloud type
context.awsAccounts = context.allAccounts.filter(acc => acc.cloudType === 'AWS');
context.azureAccounts = context.allAccounts.filter(acc => acc.cloudType === 'Azure');
context.gcpAccounts = context.allAccounts.filter(acc => acc.cloudType === 'GCP');
// Group by organization
const orgMap = new Map<string, OrganizationInfo>();
context.allAccounts.forEach(account => {
if (account.organizationId) {
if (!orgMap.has(account.organizationId)) {
orgMap.set(account.organizationId, {
organizationId: account.organizationId,
accounts: []
});
}
orgMap.get(account.organizationId)!.accounts.push(account);
}
});
context.organizations = Array.from(orgMap.values());
}
private determineDefaultAccounts(context: DiscoveredContext): void {
// AWS default: First account marked as default, or first account
if (context.awsAccounts.length > 0) {
const defaultAws = context.awsAccounts.find(acc => acc.isDefault) || context.awsAccounts[0];
context.defaultAccountByCloud['AWS'] = defaultAws;
}
// Azure default: Similar logic
if (context.azureAccounts.length > 0) {
const defaultAzure = context.azureAccounts.find(acc => acc.isDefault) || context.azureAccounts[0];
context.defaultAccountByCloud['Azure'] = defaultAzure;
}
// GCP default: Similar logic
if (context.gcpAccounts.length > 0) {
const defaultGcp = context.gcpAccounts.find(acc => acc.isDefault) || context.gcpAccounts[0];
context.defaultAccountByCloud['GCP'] = defaultGcp;
}
}
private printDiscoveryResults(context: DiscoveredContext): void {
console.log('\nπ ACCOUNT DISCOVERY RESULTS');
console.log('='.repeat(60));
console.log(`\nπ TOTAL DISCOVERED ACCOUNTS: ${context.allAccounts.length}`);
console.log(` βοΈ AWS: ${context.awsAccounts.length}`);
console.log(` βοΈ Azure: ${context.azureAccounts.length}`);
console.log(` βοΈ GCP: ${context.gcpAccounts.length}`);
if (context.organizations.length > 0) {
console.log(`\nπ’ ORGANIZATIONS: ${context.organizations.length}`);
context.organizations.forEach(org => {
console.log(` π ${org.organizationId}: ${org.accounts.length} accounts`);
});
}
console.log('\nπ― DEFAULT ACCOUNTS PER CLOUD:');
Object.entries(context.defaultAccountByCloud).forEach(([cloud, account]) => {
console.log(` ${cloud}: ${account.accountId} ${account.accountName ? `(${account.accountName})` : ''}`);
});
if (context.allAccounts.length > 0) {
console.log('\nπ ALL DISCOVERED ACCOUNTS:');
context.allAccounts.forEach(account => {
const name = account.accountName ? ` (${account.accountName})` : '';
const org = account.organizationId ? ` [Org: ${account.organizationId}]` : '';
const def = account.isDefault ? ' [DEFAULT]' : '';
console.log(` ${account.cloudType}: ${account.accountId}${name}${org}${def}`);
});
}
}
private getEnhancedTestParameters(endpoint: any, accountContext?: AccountInfo, orgContext?: OrganizationInfo): Record<string, any> {
const params: Record<string, any> = {};
// Add account context if available
if (accountContext) {
params.accountId = accountContext.accountId;
if (accountContext.organizationId) {
params.organizationId = accountContext.organizationId;
}
}
// Add organization context if available
if (orgContext) {
params.organizationId = orgContext.organizationId;
}
// Add common date parameters for cost-related endpoints
if (endpoint.path.includes('cost') || endpoint.path.includes('usage') || endpoint.path.includes('invoice')) {
params.startDate = '2024-01-01';
params.endDate = '2024-01-31';
}
// Add pagination for list endpoints
if (endpoint.path.includes('list') || endpoint.description.toLowerCase().includes('list')) {
params.limit = 10;
params.offset = 0;
}
// Add endpoint-specific parameters based on our analysis
switch (true) {
case endpoint.path.includes('/caui'):
Object.assign(params, {
costType: ['unblended'],
groupBy: ['service'],
periodGranLevel: 'daily'
});
break;
case endpoint.path.includes('/service-names/distinct'):
params.limit = 10;
break;
case endpoint.path.includes('/resource-explorer/distinct'):
params.search = 'aws'; // Provide required search parameter
break;
case endpoint.path.includes('/kubernetes'):
if (accountContext) {
params.clusterId = 'default';
params.namespace = 'default';
}
break;
}
return params;
}
async testEndpointWithContext(endpoint: any, accountContext?: AccountInfo, orgContext?: OrganizationInfo): Promise<EnhancedTestResult> {
const startTime = Date.now();
const testParams = this.getEnhancedTestParameters(endpoint, accountContext, orgContext);
try {
const contextDesc = accountContext ? `[${accountContext.cloudType}:${accountContext.accountId}]` : '[No Context]';
console.log(` π ${endpoint.method} ${endpoint.path} ${contextDesc}`);
console.log(` π ${endpoint.description}`);
if (Object.keys(testParams).length > 0) {
console.log(` π§ Params: ${Object.keys(testParams).join(', ')}`);
}
const response = await this.apiClient.makeRequest(endpoint.path, testParams);
const responseTime = Date.now() - startTime;
if (response.success) {
let dataInfo = 'No data';
if (response.data) {
if (Array.isArray(response.data)) {
dataInfo = `${response.data.length} array items`;
} else if (typeof response.data === 'object') {
dataInfo = `object with ${Object.keys(response.data).length} keys`;
}
}
console.log(` β
SUCCESS: ${dataInfo} (${responseTime}ms)`);
return {
endpoint: endpoint.path,
method: endpoint.method,
description: endpoint.description,
category: endpoint.category,
success: true,
data: response.data,
responseTime,
accountContext,
organizationContext: orgContext,
providedParams: testParams
};
} else {
console.log(` β FAILED: ${response.error} (${responseTime}ms)`);
return {
endpoint: endpoint.path,
method: endpoint.method,
description: endpoint.description,
category: endpoint.category,
success: false,
error: response.error,
responseTime,
accountContext,
organizationContext: orgContext,
providedParams: testParams
};
}
} catch (error: any) {
const responseTime = Date.now() - startTime;
console.log(` π₯ ERROR: ${error.message} (${responseTime}ms)`);
return {
endpoint: endpoint.path,
method: endpoint.method,
description: endpoint.description,
category: endpoint.category,
success: false,
error: error.message,
responseTime,
accountContext,
organizationContext: orgContext,
providedParams: testParams
};
}
}
async runEnhancedTest(credentials: { username: string; password: string }): Promise<EnhancedTestResult[]> {
console.log(`\n${'='.repeat(100)}`);
console.log(`π§ͺ ENHANCED API TEST WITH ACCOUNT DISCOVERY: ${credentials.username}`);
console.log(`π‘ Base URL: ${this.baseURL}`);
console.log(`${'='.repeat(100)}`);
// Authenticate
console.log('\nπ AUTHENTICATION');
const authSuccess = await this.authenticate(credentials);
if (!authSuccess) {
throw new Error('Authentication failed');
}
console.log(`β
Authentication successful for ${credentials.username}`);
// Discover account context
const context = await this.discoverAccountContext();
this.printDiscoveryResults(context);
// Test endpoints with discovered context
const allResults: EnhancedTestResult[] = [];
console.log(`\nπ§ͺ TESTING ENDPOINTS WITH ACCOUNT CONTEXT`);
console.log(`π Total endpoints: ${UMBRELLA_ENDPOINTS.length}`);
console.log('='.repeat(100));
// Test without context first (baseline)
console.log(`\nπ Phase 1: Testing without account context`);
for (let i = 0; i < Math.min(5, UMBRELLA_ENDPOINTS.length); i++) {
const endpoint = UMBRELLA_ENDPOINTS[i];
console.log(`\n[${i + 1}/5] ${endpoint.category} (No Context)`);
const result = await this.testEndpointWithContext(endpoint);
allResults.push(result);
await new Promise(resolve => setTimeout(resolve, 200));
}
// Test with account context for each cloud
if (Object.keys(context.defaultAccountByCloud).length > 0) {
console.log(`\nπ Phase 2: Testing with default accounts per cloud`);
for (const [cloudType, defaultAccount] of Object.entries(context.defaultAccountByCloud)) {
console.log(`\nβοΈ Testing with ${cloudType} context: ${defaultAccount.accountId}`);
// Test key endpoints with this account context
const keyEndpoints = UMBRELLA_ENDPOINTS.filter(ep =>
ep.path.includes('caui') ||
ep.path.includes('cost-and-usage') ||
ep.path.includes('budget') ||
ep.path.includes('kubernetes') ||
ep.description.toLowerCase().includes('account')
);
for (let i = 0; i < keyEndpoints.length; i++) {
const endpoint = keyEndpoints[i];
console.log(`\n[${cloudType}] ${endpoint.category}`);
const result = await this.testEndpointWithContext(endpoint, defaultAccount);
allResults.push(result);
await new Promise(resolve => setTimeout(resolve, 200));
}
}
}
return allResults;
}
printEnhancedResults(results: EnhancedTestResult[]): void {
console.log(`\n${'='.repeat(100)}`);
console.log(`π ENHANCED TEST RESULTS WITH ACCOUNT CONTEXT`);
console.log(`${'='.repeat(100)}`);
const successCount = results.filter(r => r.success).length;
const failureCount = results.length - successCount;
const successRate = (successCount / results.length) * 100;
console.log(`\nπ― OVERALL SUMMARY:`);
console.log(` β
Successful: ${successCount}/${results.length} (${successRate.toFixed(1)}%)`);
console.log(` β Failed: ${failureCount}/${results.length}`);
console.log(` β±οΈ Avg Response Time: ${Math.round(results.reduce((sum, r) => sum + r.responseTime, 0) / results.length)}ms`);
// Results with account context vs without
const withContext = results.filter(r => r.accountContext);
const withoutContext = results.filter(r => !r.accountContext);
if (withContext.length > 0 && withoutContext.length > 0) {
const contextSuccessRate = (withContext.filter(r => r.success).length / withContext.length) * 100;
const noContextSuccessRate = (withoutContext.filter(r => r.success).length / withoutContext.length) * 100;
console.log(`\nπ CONTEXT IMPACT:`);
console.log(` π― With Account Context: ${contextSuccessRate.toFixed(1)}% success`);
console.log(` π― Without Context: ${noContextSuccessRate.toFixed(1)}% success`);
console.log(` π Improvement: ${(contextSuccessRate - noContextSuccessRate).toFixed(1)}%`);
}
// Working endpoints by cloud context
if (withContext.length > 0) {
console.log(`\nβοΈ SUCCESS BY CLOUD CONTEXT:`);
const byCloud = withContext.reduce((acc: any, result) => {
if (result.accountContext) {
const cloud = result.accountContext.cloudType;
if (!acc[cloud]) acc[cloud] = { success: 0, total: 0 };
acc[cloud].total++;
if (result.success) acc[cloud].success++;
}
return acc;
}, {});
Object.entries(byCloud).forEach(([cloud, stats]: [string, any]) => {
const rate = (stats.success / stats.total) * 100;
console.log(` ${cloud}: ${stats.success}/${stats.total} (${rate.toFixed(1)}%)`);
});
}
// Show successful endpoints with context
const successfulWithContext = results.filter(r => r.success && r.accountContext);
if (successfulWithContext.length > 0) {
console.log(`\nβ
ENDPOINTS WORKING WITH ACCOUNT CONTEXT:`);
successfulWithContext.forEach(result => {
const cloud = result.accountContext!.cloudType;
const accountId = result.accountContext!.accountId;
console.log(` ${result.method} ${result.endpoint} [${cloud}:${accountId}]`);
});
}
}
}
async function main() {
console.log('π UMBRELLA MCP API TESTING WITH ACCOUNT DISCOVERY');
console.log('==================================================');
console.log(`π
Test Time: ${new Date().toISOString()}`);
const baseURL = process.env.UMBRELLA_API_BASE_URL || 'https://api.umbrellacost.io/api/v1';
const credentialSets = getTestCredentialSets();
if (!credentialSets) {
console.error('β No test credentials available');
process.exit(1);
}
for (let i = 0; i < credentialSets.length; i++) {
const creds = credentialSets[i];
const credName = `Account ${i + 1} (${creds.username})`;
try {
const tester = new AccountDiscoveryTester(baseURL);
const results = await tester.runEnhancedTest(creds);
tester.printEnhancedResults(results);
} catch (error: any) {
console.error(`β Enhanced test failed for ${credName}: ${error.message}`);
}
// Pause between accounts
if (i < credentialSets.length - 1) {
console.log('\nβ³ Pausing before testing next account...');
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
console.log(`\nπ ENHANCED TESTING WITH ACCOUNT DISCOVERY COMPLETED!`);
}
// Run main function if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}
export { AccountDiscoveryTester, AccountInfo, OrganizationInfo, DiscoveredContext, EnhancedTestResult };