Skip to main content
Glama
server.ts.broken•70.8 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { UserSessionManager, UserCredentials } from './user-session-manager.js'; import { AuthCredentials, QueryParams, QueryParamsSchema, UMBRELLA_ENDPOINTS } from './types.js'; import { CAUI_PARAMETER_GUIDE, COMMON_QUERY_PATTERNS, mapQuestionToParameters } from './caui-parameter-guide.js'; // Note: Init prompts are now loaded from init_prompt.txt file import { readFileSync } from 'fs'; import { resolve } from 'path'; export class UmbrellaMcpServer { private server: Server; private sessionManager: UserSessionManager; private baseURL: string; private initPromptGuidelines: string; constructor(baseURL: string) { this.baseURL = baseURL; this.sessionManager = new UserSessionManager(baseURL); this.initPromptGuidelines = this.loadInitPromptGuidelines(); this.server = new Server( { name: process.env.MCP_SERVER_NAME || 'Umbrella MCP', version: process.env.MCP_SERVER_VERSION || '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private loadInitPromptGuidelines(): string { try { const initPromptPath = resolve(process.cwd(), 'init_prompt.txt'); return readFileSync(initPromptPath, 'utf-8'); } catch (error) { console.error('āš ļø Warning: Could not load init_prompt.txt, using default guidance'); return 'Default Umbrella Cost API guidelines apply.'; } } private setupToolHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { const authTool: Tool = { name: 'authenticate_user', description: 'Authenticate with Umbrella Cost API using username and password (multi-tenant)', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Your Umbrella Cost username (email address)', }, password: { type: 'string', description: 'Your Umbrella Cost password', }, sessionId: { type: 'string', description: 'Optional: Custom session identifier (defaults to username-based ID)', }, }, required: ['username', 'password'], }, }; const logoutTool: Tool = { name: 'logout', description: 'Log out and remove user session', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to logout (optional - will logout current session if not provided)', }, }, required: [], }, }; const sessionStatusTool: Tool = { name: 'session_status', description: 'Check authentication status and session information', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to check status for (optional)', }, }, required: [], }, }; const apiTools: Tool[] = UMBRELLA_ENDPOINTS.map((endpoint) => ({ name: `api__${endpoint.path.replace(/\//g, '_').replace(/[-]/g, '_')}`, description: `${endpoint.description} (${endpoint.category})`, inputSchema: { type: 'object', properties: { ...(endpoint.parameters && Object.keys(endpoint.parameters).length > 0 ? Object.entries(endpoint.parameters).reduce((acc, [key, desc]) => { acc[key] = { type: 'string', description: desc, }; return acc; }, {} as Record<string, any>) : {}), }, required: [], }, })); const listEndpointsTool: Tool = { name: 'list_endpoints', description: 'List all available Umbrella Cost API endpoints with their descriptions', inputSchema: { type: 'object', properties: {}, required: [], }, }; const helpTool: Tool = { name: 'help', description: 'Get help and usage information for the Umbrella MCP server', inputSchema: { type: 'object', properties: { topic: { type: 'string', description: 'Specific help topic (authentication, endpoints, parameters)', }, }, required: [], }, }; const initGuidanceTool: Tool = { name: 'get_umbrella_guidance', description: 'Get smart API decision guidance and initialization prompt for Umbrella Cost Management', inputSchema: { type: 'object', properties: { query_type: { type: 'string', description: 'Type of query (cost, recommendations, anomalies, service-analysis)', }, service_name: { type: 'string', description: 'Name of cloud service to analyze (e.g., redshift, bigquery, s3)', }, }, required: [], }, }; const apiArtifactTool: Tool = { name: 'get_umbrella_api_artifact', description: '🚨 CRITICAL: Get comprehensive Umbrella API reference - REQUIRED for amortized costs. Contains exact parameter mappings that desktop client MUST use.', inputSchema: { type: 'object', properties: { section: { type: 'string', description: 'Specific section (cost-types, parameters, examples, validation) or full artifact', }, }, required: [], }, }; const prerequisiteCheckTool: Tool = { name: 'check_amortized_prerequisites', description: '🚨 REQUIRED before any amortized cost queries. Checks if you have the artifact needed for correct parameters.', inputSchema: { type: 'object', properties: { query_type: { type: 'string', description: 'Type of cost query (amortized, net_amortized, unblended)', }, }, required: ['query_type'], }, }; const validateParametersTool: Tool = { name: 'validate_caui_parameters', description: 'Validate api___invoices_caui parameters before making API call. Prevents common mistakes like wrong cost type parameters.', inputSchema: { type: 'object', properties: { parameters: { type: 'object', description: 'Parameters to validate for api___invoices_caui call', }, query_intent: { type: 'string', description: 'What the user is trying to achieve (e.g., "get amortized costs", "net amortized costs")', }, }, required: ['parameters'], }, }; return { tools: [authTool, logoutTool, sessionStatusTool, listEndpointsTool, helpTool, initGuidanceTool, apiArtifactTool, prerequisiteCheckTool, validateParametersTool, ...apiTools], }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Authentication tools if (name === 'authenticate_user') { return await this.handleAuthenticateUser(args as any); } if (name === 'logout') { return this.handleLogout(args as any); } if (name === 'session_status') { return this.handleSessionStatus(args as any); } // List endpoints tool if (name === 'list_endpoints') { return this.handleListEndpoints(); } // Help tool if (name === 'help') { return this.handleHelp(args?.topic as string); } // Umbrella guidance tool if (name === 'get_umbrella_guidance') { return this.handleUmbrellaGuidance(args as any); } // Umbrella API artifact tool if (name === 'get_umbrella_api_artifact') { return this.handleUmbrellaApiArtifact(args as any); } // Prerequisite check tool if (name === 'check_amortized_prerequisites') { return this.handlePrerequisiteCheck(args as any); } // Parameter validation tool if (name === 'validate_caui_parameters') { return this.handleParameterValidation(args as any); } // API endpoint tools if (name.startsWith('api___')) { // Convert tool name back to endpoint path by reversing the conversion logic // Tool generation: `api__${endpoint.path.replace(/\//g, '_').replace(/[-]/g, '_')}` // So we need to reverse this process carefully const toolNamePart = name.replace('api___', ''); const endpoint = UMBRELLA_ENDPOINTS.find(ep => { const expectedToolName = `api__${ep.path.replace(/\//g, '_').replace(/[-]/g, '_')}`; return expectedToolName === name; }); if (!endpoint) { return { content: [ { type: 'text', text: `Error: Unknown tool "${name}". Use "list_endpoints" to see available endpoints.`, }, ], }; } return await this.handleApiCall(endpoint.path, args); } return { content: [ { type: 'text', text: `Error: Unknown tool "${name}". Use "help" to see available tools.`, }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); } private async handleAuthenticateUser(args: { username: string; password: string; sessionId?: string }) { try { // Validate that credentials are provided if (!args.username || !args.password) { return { content: [ { type: 'text', text: `āŒ Authentication failed: Both username and password are required.\n\nPlease provide your Umbrella Cost credentials:\n- Username: Your email address\n- Password: Your account password`, }, ], }; } const credentials: UserCredentials = { username: args.username, password: args.password }; const result = await this.sessionManager.authenticateUser(credentials, args.sessionId); if (result.success) { const stats = this.sessionManager.getStats(); return { content: [ { type: 'text', text: `āœ… Successfully authenticated as ${args.username}\n\n**Session ID:** ${result.sessionId}\n\nYour credentials have been securely processed and a temporary access token has been obtained.\n\nYou can now use the API tools to query your Umbrella Cost data. Use "list_endpoints" to see available endpoints.\n\nšŸ“Š **Server Status:** ${stats.activeSessions} active session(s)\n\n## šŸ“‹ Umbrella Cost API Guidelines\n\nFor this session, please follow these guidelines for all future responses:\n\n${this.initPromptGuidelines}`, }, ], }; } else { return { content: [ { type: 'text', text: `āŒ Authentication failed: ${result.error}\n\nPlease verify your credentials and try again:\n- Ensure your username is your full email address\n- Check your password is correct\n- Confirm your account has API access permissions\n\n**Note:** Your credentials are only used to obtain a secure token and are not stored.`, }, ], }; } } catch (error: any) { return { content: [ { type: 'text', text: `āŒ Authentication error: ${error.message}`, }, ], }; } } private handleLogout(args: { username?: string }) { try { const stats = this.sessionManager.getStats(); if (args.username) { // Logout specific user const removed = this.sessionManager.removeUserSession(args.username); if (removed) { return { content: [ { type: 'text', text: `āœ… Successfully logged out user: ${args.username}\n\nšŸ“Š **Server Status:** ${stats.activeSessions - 1} active session(s) remaining`, }, ], }; } else { return { content: [ { type: 'text', text: `āš ļø No active session found for user: ${args.username}`, }, ], }; } } else { // Show active sessions for admin purposes const activeSessions = this.sessionManager.getActiveSessions(); if (activeSessions.length === 0) { return { content: [ { type: 'text', text: `ā„¹ļø No active sessions to logout.`, }, ], }; } else { let output = `šŸ“Š **Active Sessions (${activeSessions.length}):**\n\n`; activeSessions.forEach(session => { output += `- **${session.username}** (${session.id})\n`; output += ` Last activity: ${session.lastActivity.toISOString()}\n\n`; }); output += `Use \`logout(username="user@email.com")\` to logout a specific user.`; return { content: [ { type: 'text', text: output, }, ], }; } } } catch (error: any) { return { content: [ { type: 'text', text: `āŒ Logout error: ${error.message}`, }, ], }; } } private handleSessionStatus(args: { username?: string }) { try { const stats = this.sessionManager.getStats(); if (args.username) { // Check specific user session const session = this.sessionManager.getUserSessionByUsername(args.username); if (session) { return { content: [ { type: 'text', text: `āœ… **Session Status for ${args.username}:**\n\n- **Status:** Authenticated\n- **Session ID:** ${session.id}\n- **Created:** ${session.createdAt.toISOString()}\n- **Last Activity:** ${session.lastActivity.toISOString()}\n- **Active:** Yes`, }, ], }; } else { return { content: [ { type: 'text', text: `āŒ **No active session found for:** ${args.username}\n\nUse \`authenticate_user\` to create a session.`, }, ], }; } } else { // Show overall server status const activeSessions = this.sessionManager.getActiveSessions(); let output = `šŸ“Š **Multi-Tenant Server Status:**\n\n`; output += `- **Total Sessions:** ${stats.totalSessions}\n`; output += `- **Active Sessions:** ${stats.activeSessions}\n`; output += `- **Expired Sessions:** ${stats.expiredSessions}\n\n`; if (activeSessions.length > 0) { output += `**Active Users:**\n`; activeSessions.forEach(session => { output += `- ${session.username} (active ${Math.round((new Date().getTime() - session.lastActivity.getTime()) / 1000 / 60)} min ago)\n`; }); } else { output += `**No active user sessions.**\n\nUse \`authenticate_user\` to create a session.`; } return { content: [ { type: 'text', text: output, }, ], }; } } catch (error: any) { return { content: [ { type: 'text', text: `āŒ Session status error: ${error.message}`, }, ], }; } } private handleListEndpoints() { const endpointsByCategory = UMBRELLA_ENDPOINTS.reduce((acc, endpoint) => { if (!acc[endpoint.category]) { acc[endpoint.category] = []; } acc[endpoint.category].push(endpoint); return acc; }, {} as Record<string, typeof UMBRELLA_ENDPOINTS>); let output = '# Available Umbrella Cost API Endpoints\n\n'; Object.entries(endpointsByCategory).forEach(([category, endpoints]) => { output += `## ${category}\n\n`; endpoints.forEach((endpoint) => { const toolName = `api_${endpoint.path.replace(/\//g, '_').replace(/[-]/g, '_')}`; output += `### ${toolName}\n`; output += `**Path:** \`${endpoint.method} ${endpoint.path}\`\n`; output += `**Description:** ${endpoint.description}\n`; if (endpoint.parameters && Object.keys(endpoint.parameters).length > 0) { output += `**Parameters:**\n`; Object.entries(endpoint.parameters).forEach(([param, desc]) => { output += `- \`${param}\`: ${desc}\n`; }); } output += '\n'; }); }); return { content: [ { type: 'text', text: output, }, ], }; } private handleHelp(topic?: string) { let helpText = ''; if (!topic || topic === 'general') { helpText = `# Umbrella MCP Server Help\n\n`; helpText += `This MCP server provides read-only access to the Umbrella Cost finops SaaS platform.\n\n`; helpText += `## Getting Started\n`; helpText += `1. First, authenticate using your Umbrella Cost credentials:\n`; helpText += ` \`\`\`\n authenticate(username="your-email@domain.com", password="your-password")\n \`\`\`\n\n`; helpText += `2. **VALIDATE PARAMETERS** (prevents common mistakes):\n`; helpText += ` \`\`\`\n validate_caui_parameters(parameters={your_params}, query_intent="amortized costs")\n \`\`\`\n`; helpText += ` This catches parameter errors BEFORE making API calls.\n\n`; helpText += `3. **GET COMPREHENSIVE API DOCUMENTATION** (for complex queries):\n`; helpText += ` \`\`\`\n get_umbrella_api_artifact()\n \`\`\`\n`; helpText += ` This provides complete API reference with examples for cost types, parameters, and mappings.\n\n`; helpText += `4. Get smart API guidance (for quick decisions):\n`; helpText += ` \`\`\`\n get_umbrella_guidance(query_type="cost", service_name="redshift")\n \`\`\`\n\n`; helpText += `5. List available endpoints:\n`; helpText += ` \`\`\`\n list_endpoints()\n \`\`\`\n\n`; helpText += `6. Make API calls to retrieve your cloud cost data.\n\n`; helpText += `## Available Help Topics\n`; helpText += `- \`authentication\`: How to authenticate and manage credentials\n`; helpText += `- \`endpoints\`: Information about available API endpoints\n`; helpText += `- \`parameters\`: How to use query parameters\n`; helpText += `- \`artifact\`: Complete API documentation and examples\n\n`; } else if (topic === 'authentication') { helpText = `# Authentication Help\n\n`; helpText += `To use the Umbrella MCP server, you need to authenticate with your Umbrella Cost credentials.\n\n`; helpText += `## Credentials Setup\n`; helpText += `You need to provide your own Umbrella Cost credentials:\n\n`; helpText += `**Authentication Process:**\n`; helpText += `- Use your registered Umbrella Cost email address as username\n`; helpText += `- Use your account password\n`; helpText += `- Both MSP and Direct customer accounts are supported\n\n`; helpText += `## Authentication Process\n`; helpText += `1. Call the \`authenticate\` tool with your username and password\n`; helpText += `2. The server will obtain an authentication token from Umbrella Cost\n`; helpText += `3. The token will be used automatically for all subsequent API calls\n\n`; helpText += `## Security Notes\n`; helpText += `- Your credentials are only used to obtain a token and are not stored\n`; helpText += `- All API access is read-only for security\n`; helpText += `- Tokens expire after a certain time and you may need to re-authenticate\n\n`; } else if (topic === 'endpoints') { helpText = `# API Endpoints Help\n\n`; helpText += `The Umbrella MCP server provides access to various Umbrella Cost API endpoints organized by category:\n\n`; helpText += `## Categories\n`; helpText += `- **Cost Analysis**: Core cost and usage data\n`; helpText += `- **Usage Analysis**: Detailed resource usage information\n`; helpText += `- **Resource Explorer**: Resource-level cost breakdown\n`; helpText += `- **Dashboards**: Dashboard configurations\n`; helpText += `- **Budget Management**: Budget tracking and alerts\n`; helpText += `- **Recommendations**: Cost optimization suggestions\n`; helpText += `- **Anomaly Detection**: Cost anomaly identification\n`; helpText += `- **Commitment Analysis**: Reserved instances and savings plans\n`; helpText += `- **Kubernetes**: Container cost analysis\n`; helpText += `- **User Management**: User and customer information\n\n`; helpText += `Use \`list_endpoints()\` to see all available endpoints with descriptions.\n\n`; } else if (topic === 'parameters') { helpText = `# Query Parameters Help\n\n`; helpText += `Many API endpoints accept query parameters to filter and customize the results:\n\n`; helpText += `## Common Parameters\n`; helpText += `- \`startDate\`: Start date for queries (YYYY-MM-DD format)\n`; helpText += `- \`endDate\`: End date for queries (YYYY-MM-DD format)\n`; helpText += `- \`accountId\`: Filter by specific AWS account ID\n`; helpText += `- \`region\`: Filter by AWS region\n`; helpText += `- \`serviceNames\`: Filter by service names (array)\n`; helpText += `- \`limit\`: Limit the number of results returned\n`; helpText += `- \`offset\`: Skip a number of results (for pagination)\n\n`; helpText += `## Example Usage\n`; helpText += '```\n'; helpText += 'api__invoices_caui(\n'; helpText += ' startDate="2024-01-01",\n'; helpText += ' endDate="2024-01-31",\n'; helpText += ' accountId="123456789012"\n'; helpText += ')\n'; helpText += '```\n\n'; helpText += `Each endpoint may have specific parameters. Check the endpoint description for details.\n\n`; } else if (topic === 'artifact') { helpText = `# API Artifact Help\n\n`; helpText += `The \`get_umbrella_api_artifact\` tool provides comprehensive API documentation designed specifically for Claude Desktop integration.\n\n`; helpText += `## What's in the Artifact?\n`; helpText += `- **Complete cost type documentation** (unblended, regular amortized, net amortized, net unblended)\n`; helpText += `- **Natural language → parameter mappings** (e.g., "amortized costs" → isAmortized: "true")\n`; helpText += `- **Expected cost values** for verification (March 2025 examples)\n`; helpText += `- **JSON API call examples** for every cost type\n`; helpText += `- **Validation rules** to prevent common errors\n`; helpText += `- **Query pattern templates** for complex scenarios\n\n`; helpText += `## Usage\n`; helpText += '```\n'; helpText += `# Get full artifact (recommended for first use)\n`; helpText += `get_umbrella_api_artifact()\n\n`; helpText += `# Get specific sections\n`; helpText += `get_umbrella_api_artifact(section="cost-types")\n`; helpText += `get_umbrella_api_artifact(section="examples")\n`; helpText += `get_umbrella_api_artifact(section="validation")\n`; helpText += '```\n\n'; helpText += `## When to Use\n`; helpText += `- **First time working with amortized costs**: Get the full artifact\n`; helpText += `- **Need parameter mappings**: Contains comprehensive natural language mappings\n`; helpText += `- **Want complete examples**: JSON examples for all cost types and scenarios\n`; helpText += `- **Self-contained documentation**: No need for multiple guidance requests\n\n`; helpText += `## Amortized Costs Specifically\n`; helpText += `The artifact completely solves desktop client issues with amortized costs by providing:\n`; helpText += `- Exact parameter names: isAmortized, isNetAmortized, isNetUnblended\n`; helpText += `- Expected values for verification\n`; helpText += `- Complete JSON examples for each cost type\n`; helpText += `- Natural language mappings for user queries\n\n`; } else { helpText = `Unknown help topic: "${topic}". Available topics: general, authentication, endpoints, parameters, artifact`; } return { content: [ { type: 'text', text: helpText, }, ], }; } private handleUmbrellaGuidance(args: any) { const { query_type, service_name } = args || {}; let guidance = this.initPromptGuidelines + '\n\n'; // Add specific guidance based on query type and service if (service_name) { guidance += `## šŸŽÆ Service-Specific Guidance for "${service_name}"\n\n`; // Basic cloud service mapping const awsServices = ['redshift', 'ec2', 's3', 'rds', 'lambda', 'cloudfront']; const gcpServices = ['bigquery', 'compute', 'storage', 'cloud-sql']; const azureServices = ['sql-database', 'virtual-machines', 'blob-storage']; let clouds: string[] = []; if (awsServices.includes(service_name.toLowerCase())) { clouds = ['aws']; } else if (gcpServices.includes(service_name.toLowerCase())) { clouds = ['gcp']; } else if (azureServices.includes(service_name.toLowerCase())) { clouds = ['azure']; } else { clouds = ['aws', 'gcp', 'azure']; // Default to all clouds } if (clouds.length === 1) { guidance += `**Smart Decision**: ${service_name} is only available in ${clouds[0].toUpperCase()}\n`; guidance += `**Recommendation**: Query only ${clouds[0].toUpperCase()} endpoints to avoid unnecessary API calls\n\n`; } else { guidance += `**Smart Decision**: ${service_name} is available across multiple clouds: ${clouds.join(', ').toUpperCase()}\n`; guidance += `**Recommendation**: Query all relevant cloud providers\n\n`; } } if (query_type) { guidance += `## šŸ“‹ Query Type Guidance: "${query_type}"\n\n`; switch (query_type.toLowerCase()) { case 'cost': guidance += `**Default Cost Type**: Unblended costs (actual charges without RI/SP amortization)\n`; guidance += `**Cost Options Available**: unblended (default), regular amortized (isAmortized=true), net amortized (isNetAmortized=true)\n`; guidance += `**Always mention**: Include cost type in your response\n`; guidance += `**🚨 CRITICAL NO-FALLBACK RULE**: If cost API fails, DO NOT try other endpoints. Return failure directly.\n`; guidance += `**Example**: "Based on unblended costs, your EC2 spending was $104,755"\n\n`; // Add detailed parameter guidance for api___invoices_caui guidance += `## šŸ”§ API Parameter Mapping for api___invoices_caui\n\n`; guidance += `**REQUIRED PARAMETERS:**\n`; guidance += `• **accountId**: ALWAYS required - use authenticated user's account ID\n`; guidance += `• **startDate/endDate**: Use YYYY-MM-DD format\n\n`; guidance += `**DATE MAPPINGS:**\n`; guidance += `• "last month" → First day of previous month to last day of previous month\n`; guidance += `• "last 3 months" → 3 months ago day 1 to today\n`; guidance += `• "last 6 months" → 6 months ago day 1 to today\n`; guidance += `• "this month" → First day of current month to today\n`; guidance += `• "YTD" → Current year-01-01 to today\n`; guidance += `• "march 2025" → 2025-03-01 to 2025-03-31\n\n`; guidance += `**GRANULARITY MAPPINGS:**\n`; guidance += `• "daily costs" → periodGranLevel: "day"\n`; guidance += `• "monthly costs" → periodGranLevel: "month"\n`; guidance += `• "quarterly costs" → periodGranLevel: "quarter"\n`; guidance += `• "total cost" → periodGranLevel: "month" (default)\n\n`; guidance += `**GROUPING MAPPINGS:**\n`; guidance += `• "total cost" → groupBy: "none"\n`; guidance += `• "cost by service" → groupBy: "service"\n`; guidance += `• "by region" → groupBy: "region"\n`; guidance += `• "by account" → groupBy: "account"\n\n`; guidance += `**šŸ”’ MANDATORY COST TYPE MAPPINGS (ONLY THESE WORK):**\n`; guidance += `• "amortized costs" → isAmortized: "true" (ONLY method)\n`; guidance += `• "net amortized" → isNetAmortized: "true" (ONLY method)\n`; guidance += `• "net amortized costs" → isNetAmortized: "true" (ONLY method)\n`; guidance += `• "actual costs" → isNetAmortized: "true" (ONLY method)\n`; guidance += `• "after discounts" → isNetAmortized: "true" (ONLY method)\n`; guidance += `• "unblended" → no parameters (default)\n`; guidance += `• āŒ NEVER use costType or isUnblended parameters\n\n`; guidance += `**VALIDATION RULES:**\n`; guidance += `• Only ONE cost type parameter at a time (mutually exclusive)\n`; guidance += `• accountId is MANDATORY - API fails without it\n`; guidance += `• Use cloud_context: "aws" when user specifies AWS\n`; guidance += `• endDate must be >= startDate\n\n`; guidance += `**COMMON PATTERNS:**\n`; guidance += `• "What is my total cost?" → accountId + current month dates + groupBy: "none"\n`; guidance += `• "Show monthly costs last 6 months" → accountId + 6-month period + periodGranLevel: "month"\n`; guidance += `• "Cost breakdown by service" → accountId + groupBy: "service"\n`; guidance += `• "Net amortized costs" → accountId + isNetAmortized: "true"\n\n`; guidance += `**EXAMPLE API CALLS:**\n`; guidance += `• Query: "AWS costs for March 2024"\n`; guidance += ` Parameters: {accountId: "USER_ACCOUNT_ID", startDate: "2024-03-01", endDate: "2024-03-31", cloud_context: "aws"}\n\n`; guidance += `• Query: "Net amortized costs last 3 months by service"\n`; guidance += ` Parameters: {accountId: "USER_ACCOUNT_ID", startDate: "3-months-ago", endDate: "today", isNetAmortized: "true", groupBy: "service"}\n\n`; guidance += `• Query: "Daily costs this month"\n`; guidance += ` Parameters: {accountId: "USER_ACCOUNT_ID", startDate: "month-start", endDate: "today", periodGranLevel: "day"}\n\n`; guidance += `**🚨 CRITICAL REMINDERS:**\n`; guidance += `• NEVER guess or estimate costs - always use exact API data\n`; guidance += `• ALWAYS include accountId: use authenticated user's account ID\n`; guidance += `• Use YYYY-MM-DD format for all dates\n`; guidance += `• Only one cost type parameter (isAmortized, isNetAmortized, or isNetUnblended) at a time\n`; guidance += `• If API fails, return failure - don't try fallback endpoints\n\n`; guidance += `**🚨 CRITICAL: GET ARTIFACT FOR AMORTIZED COSTS:**\n`; guidance += `Desktop client MUST call: get_umbrella_api_artifact()\n`; guidance += `This is REQUIRED for correct amortized cost parameters.\n`; guidance += `Without the artifact, you will get wrong parameters and unblended costs.\n\n`; break; case 'recommendations': case 'anomalies': guidance += `**Account Requirement**: Use specific accounts only, NOT "ALL ACCOUNTS"\n`; guidance += `**Reason**: ${query_type} analysis requires individual account data\n`; if (query_type.toLowerCase() === 'anomalies') { guidance += `**Default Filter**: Show only OPEN anomalies (exclude closed unless requested)\n`; } guidance += `**Suggestion**: If user asks for aggregated data, explain limitation and ask for specific account\n\n`; break; case 'service-analysis': guidance += `**Smart Filtering**: Use cloud service mapping to target correct providers\n`; guidance += `**Cost Type**: Default to cost + discount with explanation\n`; guidance += `**Account Strategy**: Use specific accounts for detailed analysis\n\n`; break; } } return { content: [ { type: 'text', text: guidance, }, ], }; } private handleUmbrellaApiArtifact(args: any) { const { section } = args || {}; try { const artifactPath = resolve(process.cwd(), 'umbrella-api-artifact.md'); const fullArtifact = readFileSync(artifactPath, 'utf-8'); if (!section || section === 'full') { return { content: [ { type: 'text', text: fullArtifact, }, ], }; } // Return specific section if requested const sectionMap: { [key: string]: string } = { 'cost-types': '## Cost Type Parameters', 'parameters': '## Natural Language → API Parameter Mapping', 'examples': '## Common API Call Examples', 'validation': '## Validation Rules', 'templates': '## Query Pattern Templates' }; const sectionHeader = sectionMap[section]; if (sectionHeader) { const lines = fullArtifact.split('\n'); const startIndex = lines.findIndex(line => line.startsWith(sectionHeader)); if (startIndex >= 0) { const nextSectionIndex = lines.findIndex((line, index) => index > startIndex && line.startsWith('##') && !line.startsWith('###') ); const endIndex = nextSectionIndex >= 0 ? nextSectionIndex : lines.length; const sectionContent = lines.slice(startIndex, endIndex).join('\n'); return { content: [ { type: 'text', text: sectionContent, }, ], }; } } // If section not found, return full artifact return { content: [ { type: 'text', text: fullArtifact, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `# Umbrella API Artifact (Embedded Fallback) ## Critical Parameters for api___invoices_caui **ALWAYS REQUIRED:** - accountId: "YOUR_ACCOUNT_ID" **Cost Types (Mutually Exclusive):** - Default: No parameter (unblended costs) - Regular Amortized: isAmortized: "true" - Net Amortized: isNetAmortized: "true" (actual amount paid) - Net Unblended: isNetUnblended: "true" **Example Cost Comparison:** - Unblended: $12,500.00 - Regular Amortized: $13,200.00 - Net Amortized: $8,750.00 - Net Unblended: $11,800.00 **Example API Call for Regular Amortized:** \`\`\`json { "name": "api___invoices_caui", "arguments": { "accountId": "YOUR_ACCOUNT_ID", "startDate": "2024-03-01", "endDate": "2024-03-31", "isAmortized": "true" } } \`\`\` Note: Could not load full artifact file. This is the essential information for amortized costs.`, }, ], }; } } private handlePrerequisiteCheck(args: any) { const { query_type } = args || {}; let response = '# Amortized Cost Prerequisites Check\n\n'; if (query_type?.toLowerCase().includes('amortized')) { response += '## 🚨 AMORTIZED COSTS DETECTED\n\n'; response += '**Status**: You are requesting amortized costs.\n\n'; response += '**CRITICAL REQUIREMENT**: You MUST call `get_umbrella_api_artifact()` first.\n\n'; response += '**Why?**\n'; response += '- Desktop clients commonly use wrong parameters for amortized costs\n'; response += '- The artifact contains the EXACT parameter mappings you need\n'; response += '- Without it, you will get unblended costs instead of amortized costs\n\n'; response += '**Next Steps:**\n'; response += '1. šŸ”“ **STOP** - Do not make API calls yet\n'; response += '2. šŸ“š **CALL**: `get_umbrella_api_artifact()`\n'; response += '3. šŸ” **FIND**: "amortized costs" → `isAmortized: "true"` mapping\n'; response += '4. āœ… **USE**: Exact parameters from artifact\n\n'; response += '**Expected Results After Following Artifact:**\n'; response += '- March 2025 Regular Amortized: $108,831.79\n'; response += '- March 2025 Net Amortized: $64,730.56\n'; response += '- Server will say "regular amortized costs" (not "unblended costs")\n\n'; } else { response += '## āœ… NON-AMORTIZED QUERY\n\n'; response += '**Status**: Standard cost query detected.\n'; response += '**Action**: You may proceed with standard parameters.\n'; response += '**Optional**: Still recommended to use `get_umbrella_api_artifact()` for best practices.\n\n'; } response += '## šŸŽÆ Critical Reminder\n'; response += 'Based on recent MCP logs, desktop clients are using wrong parameters:\n'; response += '- āŒ `costType: "cost"` → IGNORED by API\n'; response += '- āŒ `isUnblended: "false"` → DOESN\'T work\n'; response += '- āœ… `isAmortized: "true"` → CORRECT for amortized costs\n\n'; response += '**The artifact solves this problem completely.**\n'; return { content: [ { type: 'text', text: response, }, ], }; } private handleParameterValidation(args: any) { const { parameters, query_intent } = args || {}; let validationResult = '# Parameter Validation Report\n\n'; const issues: string[] = []; const fixes: string[] = []; // Check for required accountId if (!parameters.accountId) { issues.push('āŒ **Missing accountId** - This is MANDATORY and causes API to return 500 errors'); fixes.push('āœ… **Add**: `"accountId": "USER_ACCOUNT_ID"`'); } // Check for wrong cost type parameters if (parameters.costType) { issues.push('āŒ **Wrong Parameter**: `costType` is IGNORED by the API'); fixes.push('āœ… **Remove**: `costType` parameter completely'); if (query_intent?.toLowerCase().includes('amortized')) { fixes.push('āœ… **Add**: `"isAmortized": "true"` for regular amortized costs'); } if (query_intent?.toLowerCase().includes('net amortized')) { fixes.push('āœ… **Add**: `"isNetAmortized": "true"` for net amortized costs'); } } // Check for conflicting parameters if (parameters.isUnblended && (parameters.isAmortized || parameters.isNetAmortized || parameters.isNetUnblended)) { issues.push('āŒ **Parameter Conflict**: Cannot use `isUnblended` with cost type parameters'); fixes.push('āœ… **Remove**: `isUnblended` parameter when using cost types'); } // Check cost type intent mapping if (query_intent) { validationResult += `## Query Intent Analysis\n`; validationResult += `**User wants**: "${query_intent}"\n\n`; if (query_intent.toLowerCase().includes('amortized') && !query_intent.toLowerCase().includes('net')) { validationResult += `**Correct Parameter**: \`isAmortized: "true"\`\n`; if (!parameters.isAmortized) { issues.push('āŒ **Missing**: `isAmortized: "true"` for regular amortized costs'); fixes.push('āœ… **Add**: `"isAmortized": "true"`'); } } else if (query_intent.toLowerCase().includes('net amortized')) { validationResult += `**Correct Parameter**: \`isNetAmortized: "true"\`\n`; if (!parameters.isNetAmortized) { issues.push('āŒ **Missing**: `isNetAmortized: "true"` for net amortized costs'); fixes.push('āœ… **Add**: `"isNetAmortized": "true"`'); } } } // Display current parameters validationResult += `\n## Current Parameters\n\`\`\`json\n${JSON.stringify(parameters, null, 2)}\n\`\`\`\n\n`; // Display issues if (issues.length > 0) { validationResult += `## āŒ Issues Found (${issues.length})\n`; issues.forEach(issue => validationResult += `${issue}\n`); validationResult += '\n'; } // Display fixes if (fixes.length > 0) { validationResult += `## āœ… Required Fixes\n`; fixes.forEach(fix => validationResult += `${fix}\n`); validationResult += '\n'; } // Show corrected parameters if (issues.length > 0) { const correctedParams = { ...parameters }; // Apply automatic fixes if (!correctedParams.accountId) { correctedParams.accountId = 'USER_ACCOUNT_ID'; } if (correctedParams.costType) { delete correctedParams.costType; } if (query_intent?.toLowerCase().includes('net amortized') && !correctedParams.isNetAmortized) { correctedParams.isNetAmortized = 'true'; } else if (query_intent?.toLowerCase().includes('amortized') && !query_intent.toLowerCase().includes('net') && !correctedParams.isAmortized) { correctedParams.isAmortized = 'true'; } if (correctedParams.isUnblended && (correctedParams.isAmortized || correctedParams.isNetAmortized)) { delete correctedParams.isUnblended; } validationResult += `## šŸ”§ Corrected Parameters\n`; validationResult += `\`\`\`json\n${JSON.stringify(correctedParams, null, 2)}\n\`\`\`\n\n`; } else { validationResult += `## āœ… Parameters Look Correct!\n`; validationResult += `No issues found. These parameters should work properly.\n\n`; } validationResult += `## šŸ’” Remember\n`; validationResult += `- Use \`get_umbrella_api_artifact()\` for complete parameter reference\n`; validationResult += `- Only ONE cost type parameter at a time\n`; validationResult += `- accountId is ALWAYS required\n`; return { content: [ { type: 'text', text: validationResult, }, ], }; } private async handleApiCall(path: string, args: any) { try { console.error(`[API-CALL] ${path}`); // Determine which user to use - check if username is provided in args let username = args?.username; let session = null; if (username) { // User explicitly provided username session = this.sessionManager.getUserSessionByUsername(username); if (!session) { return { content: [ { type: 'text', text: `āŒ No active session found for user: ${username}\n\nPlease authenticate first using \`authenticate_user\`.`, }, ], }; } } else { // No username provided - check if there's exactly one active session const activeSessions = this.sessionManager.getActiveSessions(); if (activeSessions.length === 0) { return { content: [ { type: 'text', text: `āŒ No authenticated users found.\n\nPlease authenticate first using \`authenticate_user(username="your-email@domain.com", password="your-password")\`.`, }, ], }; } else if (activeSessions.length === 1) { // Use the single active session session = this.sessionManager.getUserSessionByUsername(activeSessions[0].username); username = activeSessions[0].username; } else { // Multiple active sessions - user must specify which one let output = `āŒ Multiple users authenticated. Please specify which user:\n\n`; output += `**Active Users:**\n`; activeSessions.forEach(s => { output += `- ${s.username}\n`; }); output += `\n**Usage:** Add \`username="user@email.com"\` to your API call.`; return { content: [ { type: 'text', text: output, }, ], }; } } if (!session) { return { content: [ { type: 'text', text: `āŒ Unable to find authenticated session. Please authenticate first.`, }, ], }; } // Store original args for cloud context detection BEFORE removing cloud_context const originalArgs = args || {}; const cloudContext = originalArgs.cloud_context?.toLowerCase(); // Remove username and cloud_context from args to avoid sending them to API const { username: _, cloud_context: __, ...apiArgs } = args || {}; // Validate and parse query parameters with proper type conversion const queryParams: QueryParams = {}; if (apiArgs) { // Convert args to query params, handling type conversion for Claude Desktop Object.entries(apiArgs).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { // Handle boolean parameter conversion (Claude Desktop sends strings) if (key === 'isUnblended' || key === 'isAmortized' || key === 'isShowAmortize' || key === 'isNetAmortized' || key === 'isNetUnblended' || key === 'isPublicCost' || key === 'isDistributed' || key === 'isListUnitPrice' || key === 'isSavingsCost') { if (typeof value === 'string') { (queryParams as any)[key] = value.toLowerCase() === 'true'; } else { (queryParams as any)[key] = !!value; } } // Handle costType parameter conversion (Claude Desktop sends costType: 'amortized') else if (key === 'costType') { if (typeof value === 'string') { const costType = value.toLowerCase().trim(); // Convert costType to frontend UI boolean flags (like costTypeToFlags function) switch (costType) { case 'amortized': // Regular amortized: isAmortized = true (frontend uses &isAmortized=true) (queryParams as any)['isAmortized'] = true; break; case 'net amortized': case 'netamortized': case 'net_amortized_cost': // Net amortized: isNetAmortized = true (queryParams as any)['isNetAmortized'] = true; break; case 'net unblended': case 'netunblended': // Net unblended: isNetUnblended = true (queryParams as any)['isNetUnblended'] = true; break; case 'unblended': default: // Default unblended: no special flags needed break; } // Don't pass costType to API since it's not supported - ALWAYS skip adding costType } // NEVER add costType to queryParams regardless of type - it's not supported by API } // Handle other array parameters else if (key === 'serviceNames') { if (typeof value === 'string' && value.includes(',')) { (queryParams as any)[key] = value.split(',').map((s: string) => s.trim()); } else if (typeof value === 'string' && !value.includes(',')) { // Single value as array (queryParams as any)[key] = [value.trim()]; } else { (queryParams as any)[key] = value; } } // Handle other parameters as-is else { (queryParams as any)[key] = value; } } }); } const validatedParams = QueryParamsSchema.partial().parse(queryParams); // DISABLED: Account auto-selection (corrupts session state and breaks recommendations) // Auto-account selection has been disabled to preserve session state and prevent // recommendations from returning 0 results after cost API calls try { // Skip account auto-selection to prevent session corruption console.error('[CLOUD-CONTEXT] Account auto-selection disabled to preserve recommendations'); } catch (error) { console.error('[CLOUD-CONTEXT] Failed to auto-select account:', error); } // FRONTEND-MATCHING COST EXPLORER LOGIC: Match exactly how frontend constructs caui API calls if (path === '/invoices/caui') { // 1. CLOUD-SPECIFIC DYNAMIC GROUPBY SYSTEM: Match frontend's exact cloud-specific logic if (!validatedParams.groupBy) { // First determine the cloud type for this query let cloudTypeId = 4; // Default to MultiCloud if (validatedParams.accountId) { // DISABLED: getAccounts() corrupts session state and breaks recommendations try { // Skip getAccounts() call to prevent session corruption console.error('[CLOUD-TYPE] getAccounts() disabled to preserve recommendations - using default cloudTypeId'); } catch (error) { console.error('[CLOUD-TYPE] Failed to determine cloud type for groupBy detection'); } } // Analyze query context for intelligent groupBy detection const queryText = JSON.stringify(apiArgs).toLowerCase(); // UNIVERSAL: Total cost queries -> 'none' (single aggregated number) if (queryText.includes('total cost') || queryText.includes('what is my total') || queryText.includes('total aws cost') || queryText.includes('total gcp cost') || queryText.includes('total azure cost') || queryText.includes('my total cost')) { validatedParams.groupBy = 'none'; console.error(`[GROUPBY-DETECT] Total cost query -> groupBy: none`); } // CLOUD-SPECIFIC GROUPBY DETECTION based on cloudTypeId else { // AWS-specific (cloudTypeId = 0) if (cloudTypeId === 0) { if (queryText.includes('by region') || queryText.includes('per region') || queryText.includes('regional')) { validatedParams.groupBy = 'region'; } else if (queryText.includes('by account') || queryText.includes('linked account') || queryText.includes('per account')) { validatedParams.groupBy = 'linkedaccid'; // AWS: Linked Account } else if (queryText.includes('availability zone') || queryText.includes('by az') || queryText.includes('per az')) { validatedParams.groupBy = 'az'; } else if (queryText.includes('by operation') || queryText.includes('per operation')) { validatedParams.groupBy = 'operation'; } else if (queryText.includes('instance type') || queryText.includes('by instance')) { validatedParams.groupBy = 'instancetype'; } else if (queryText.includes('charge type') || queryText.includes('by charge')) { validatedParams.groupBy = 'chargetype'; } else if (queryText.includes('by resource') || queryText.includes('per resource')) { validatedParams.groupBy = 'resourceid'; } else { validatedParams.groupBy = 'service'; // AWS default } } // Azure-specific (cloudTypeId = 1) else if (cloudTypeId === 1) { if (queryText.includes('resource group') || queryText.includes('by resource group')) { validatedParams.groupBy = 'resourcegroup'; // Azure: Resource Group } else if (queryText.includes('subscription') || queryText.includes('by subscription')) { validatedParams.groupBy = 'linkedaccid'; // Azure: Subscription } else if (queryText.includes('meter category') || queryText.includes('by meter category')) { validatedParams.groupBy = 'metercategory'; } else if (queryText.includes('meter') || queryText.includes('by meter')) { validatedParams.groupBy = 'operation'; // Azure: Meter Name } else if (queryText.includes('by region') || queryText.includes('per region')) { validatedParams.groupBy = 'region'; } else if (queryText.includes('by resource') || queryText.includes('per resource')) { validatedParams.groupBy = 'resourceid'; } else { validatedParams.groupBy = 'service'; // Azure default } } // GCP-specific (cloudTypeId = 2) else if (cloudTypeId === 2) { if (queryText.includes('project') || queryText.includes('by project') || queryText.includes('per project')) { validatedParams.groupBy = 'linkedaccid'; // GCP: Projects (displayed as "Linked Account") } else if (queryText.includes('by region') || queryText.includes('per region')) { validatedParams.groupBy = 'region'; } else if (queryText.includes('by operation') || queryText.includes('per operation')) { validatedParams.groupBy = 'operation'; } else if (queryText.includes('cost type description') || queryText.includes('cost type desc')) { validatedParams.groupBy = 'costtypedescription'; // GCP-specific } else if (queryText.includes('by resource') || queryText.includes('per resource')) { validatedParams.groupBy = 'resourceid'; } else { validatedParams.groupBy = 'service'; // GCP default } } // MultiCloud-specific (cloudTypeId = 4) else if (cloudTypeId === 4) { if (queryText.includes('cloud provider') || queryText.includes('by cloud') || queryText.includes('per cloud')) { validatedParams.groupBy = 'cloudprovider'; // MultiCloud: Cloud Provider } else if (queryText.includes('by account') || queryText.includes('linked account') || queryText.includes('per account')) { validatedParams.groupBy = 'linkedaccid'; } else if (queryText.includes('by region') || queryText.includes('per region')) { validatedParams.groupBy = 'region'; } else if (queryText.includes('by resource') || queryText.includes('per resource')) { validatedParams.groupBy = 'resourceid'; } else { validatedParams.groupBy = 'service'; // MultiCloud default } } // Fallback else { validatedParams.groupBy = 'service'; } const cloudName = ['AWS', 'Azure', 'GCP', 'Unknown', 'MultiCloud'][cloudTypeId] || 'Unknown'; console.error(`[GROUPBY-CLOUD] ${cloudName} (${cloudTypeId}) query -> groupBy: ${validatedParams.groupBy}`); } } else { console.error(`[GROUPBY-EXPLICIT] Client provided groupBy: ${validatedParams.groupBy} -> Using as-is`); } // 2. DEFAULT AGGREGATION: day (matches frontend CostTrackingConstants.GRAN_LEVEL_DAILY) if (!validatedParams.periodGranLevel) { validatedParams.periodGranLevel = 'day'; } // 3. DEFAULT COST TYPES: ['cost', 'discount'] (matches frontend baseState.currCostType) if (!validatedParams.costType) { validatedParams.costType = ['cost', 'discount']; } // 4. COST TYPE FLAGS: Default all to false (matches frontend baseState) if (validatedParams.isShowAmortize === undefined) validatedParams.isShowAmortize = false; if (validatedParams.isNetAmortized === undefined) validatedParams.isNetAmortized = false; if (validatedParams.isNetUnblended === undefined) validatedParams.isNetUnblended = false; if (validatedParams.isPublicCost === undefined) validatedParams.isPublicCost = false; if (validatedParams.isDistributed === undefined) validatedParams.isDistributed = false; if (validatedParams.isListUnitPrice === undefined) validatedParams.isListUnitPrice = false; if (validatedParams.isSavingsCost === undefined) validatedParams.isSavingsCost = false; // 5. TAX EXCLUSION: DISABLED - This validation corrupts session state and breaks recommendations // TODO: Find a way to do tax exclusion without calling getAccounts() during validation // if (!validatedParams.excludeFilters && validatedParams.accountId) { // try { // const accountsResponse = await session.apiClient.getAccounts(); // if (accountsResponse.success && accountsResponse.data) { // let accounts = []; // if (accountsResponse.data.accounts) { // accounts = accountsResponse.data.accounts; // } else if (Array.isArray(accountsResponse.data)) { // accounts = accountsResponse.data; // } else { // accounts = [accountsResponse.data]; // } // const account = accounts.find((acc: any) => acc.accountId === validatedParams.accountId); // // // FRONTEND LOGIC: [CLOUD_TYPE_IDS.AWS].includes(cloudTypeId) ? { chargetype: ['Tax'] } : {} // if (account && account.cloudTypeId === 0) { // AWS = 0 // validatedParams.excludeFilters = { chargetype: ['Tax'] }; // } // } // } catch (error) { // console.error('[TAX-EXCLUSION] Failed to determine cloud type for tax exclusion:', error); // } // } // 6. TEMPORAL AGGREGATION: For non-service grouping, use usagedate (matches frontend parameter construction) if (validatedParams.periodGranLevel && ['month', 'quarter', 'year', 'week', 'hour'].includes(validatedParams.periodGranLevel) && validatedParams.groupBy === 'none') { validatedParams.groupBy = 'usagedate'; } } // Special handling for recommendations/report to use the working V2 API let response; if (path === '/recommendations/report' || path === '/recommendations') { response = await session.apiClient.getRecommendations(validatedParams); } else { response = await session.apiClient.makeRequest(path, validatedParams); } if (response.success) { let output = `# API Response: ${path}\n\n`; output += `**Authenticated as:** ${username}\n`; output += `**Session ID:** ${session.id}\n\n`; output += `**Status:** āœ… Success\n\n`; // Check if user requested amortized costs and add clarification const requestedAmortized = validatedParams.isUnblended === false || validatedParams.isAmortized === true || validatedParams.isShowAmortize === true || (originalArgs.isUnblended === 'false') || (originalArgs.costType === 'amortized'); // Claude Desktop costType conversion // Check if user requested net costs (these work differently) const requestedNetAmortized = validatedParams.isNetAmortized === true || (originalArgs.isNetAmortized === 'true') || (originalArgs.costType === 'net amortized') || (originalArgs.costType === 'netamortized'); // Claude Desktop costType conversion const requestedNetUnblended = validatedParams.isNetUnblended === true || (originalArgs.isNetUnblended === 'true') || (originalArgs.costType === 'net unblended') || (originalArgs.costType === 'netunblended'); // Claude Desktop costType conversion if (response.data) { // Special formatting for recommendations to show summary + top items if (Array.isArray(response.data) && path.includes('recommendations')) { const recommendations = response.data; const totalSavings = recommendations.reduce((sum: number, rec: any) => sum + (rec.annual_savings || 0), 0); // Use category breakdown from API response (calculated from ALL recommendations) const byCategory = response.category_breakdown || {}; const actualTotalSavings = response.total_potential_savings || totalSavings; output += `**Results:** ${response.total_count || recommendations.length} recommendations\n`; output += `**Total Potential Annual Savings:** $${actualTotalSavings.toLocaleString()}\n\n`; // Show category breakdown output += '**šŸ’° Breakdown by Category:**\n'; Object.entries(byCategory) .sort(([,a], [,b]) => b.amount - a.amount) .forEach(([category, {amount, count}]) => { const percentage = ((amount / actualTotalSavings) * 100).toFixed(1); output += `- **${category}**: $${amount.toLocaleString()} (${count} recs, ${percentage}%)\n`; }); output += '\n**šŸ” Top 10 Recommendations:**\n'; const topRecs = recommendations .sort((a: any, b: any) => (b.annual_savings || 0) - (a.annual_savings || 0)) .slice(0, 10); topRecs.forEach((rec: any, i: number) => { const monthly = Math.round((rec.annual_savings || 0) / 12); output += `${i + 1}. **${rec.type}** - ${rec.resource_name || rec.resource_id}\n`; output += ` šŸ’° $${(rec.annual_savings || 0).toLocaleString()}/year ($${monthly}/month)\n`; output += ` šŸ”§ ${rec.service} | ${rec.recommended_action}\n\n`; }); } // Special formatting for /users endpoint to show user-friendly account names else if (path === '/users' && typeof response.data === 'object' && response.data.accounts) { const userData = response.data; const accounts = userData.accounts || []; output += `**User:** ${userData.user_display_name || userData.user_name}\n`; output += `**Company:** ${userData.company_name}\n`; output += `**Role:** ${userData.is_admin ? 'Admin' : 'User'}\n\n`; output += `**ā˜ļø Cloud Accounts (${accounts.length}):**\n\n`; accounts.forEach((account: any, index: number) => { const cloudProvider = account.cloudTypeId === 0 ? 'AWS' : account.cloudTypeId === 1 ? 'Azure' : account.cloudTypeId === 2 ? 'GCP' : account.cloudTypeId === 4 ? 'Multi-Cloud' : 'Unknown'; output += `${index + 1}. **${account.accountName}** (${cloudProvider})\n`; output += ` - ID: ${account.accountId}\n`; output += ` - Currency: ${account.currencyCode || 'USD'}\n`; output += ` - Last Process: ${account.lastProcessTime?.split('T')[0] || 'N/A'}\n\n`; }); } else if (Array.isArray(response.data)) { output += `**Results:** ${response.data.length} items\n\n`; // Add clarification based on cost type requested if (path.includes('/invoices/caui')) { if (requestedNetAmortized && requestedNetUnblended) { output += `**šŸ’” Cost Type Note:** You requested both net amortized and net unblended costs. The values shown are net amortized costs (with RI/SP benefits and credits/discounts applied).\n\n`; } else if (requestedNetAmortized) { output += `**šŸ’” Cost Type Note:** You requested net amortized costs. The values shown are net amortized costs (with reserved instance/savings plan benefits and credits/discounts applied).\n\n`; } else if (requestedNetUnblended) { output += `**šŸ’” Cost Type Note:** You requested net unblended costs. The values shown are net unblended costs (with credits/discounts applied but without RI/SP amortization).\n\n`; } else if (requestedAmortized) { const costTypeUsed = originalArgs.costType === 'amortized' ? ` (converted from costType: "${originalArgs.costType}")` : ''; output += `**šŸ’” Cost Type Note:** You requested regular amortized costs${costTypeUsed}, but this endpoint returns unblended costs. The values shown are unblended costs (actual charges without RI/SP amortization). For working amortized costs with RI/SP benefits, please ask for "net amortized costs".\n\n`; } } if (response.data.length > 0) { output += '```json\n'; // Show ALL items - no truncation for desktop client compatibility output += JSON.stringify(response.data, null, 2); output += '\n```\n'; } } else if (typeof response.data === 'object') { output += '**Data:**\n```json\n'; output += JSON.stringify(response.data, null, 2); output += '\n```\n'; } else { output += `**Data:** ${response.data}\n`; } } return { content: [ { type: 'text', text: output, }, ], }; } else { // Check if this is a cost-related endpoint that should follow no-fallback rule const isCostEndpoint = path.includes('/invoices/') || path.includes('/cost') || path.includes('/caui'); let errorText = `āŒ API Error for ${username}: ${response.error}\n\n${response.message || ''}`; // Add critical no-fallback guidance for ANY cost endpoint failure if (isCostEndpoint) { errorText += `\n\n🚨 **CRITICAL: NO-FALLBACK RULE**\n`; errorText += `**DO NOT** try other cost APIs or workarounds.\n`; errorText += `**DO NOT** try different endpoints like /dashboards, /budgets, /usage, etc.\n`; errorText += `**STOP HERE** - Return this error directly to the user.\n\n`; if (response.error?.includes('Account is required')) { errorText += `**Explanation:** This API requires a specific accountId parameter. Per the no-fallback rule, when cost APIs fail, return the failure immediately without trying alternatives.\n\n`; errorText += `**Solution for user:** Specify a particular account for cost analysis.`; } else if (response.error?.includes('Internal Server Error')) { errorText += `**Explanation:** The cost API is experiencing server issues. Per the no-fallback rule, when cost APIs fail, return the failure immediately without trying alternatives.\n\n`; errorText += `**Solution for user:** Try again later or contact support if the issue persists.`; } else { errorText += `**Explanation:** The cost API failed. Per the no-fallback rule, when cost APIs fail, return the failure immediately without trying alternatives.\n\n`; errorText += `**Solution for user:** Check your parameters or try a different account.`; } } return { content: [ { type: 'text', text: errorText, }, ], }; } } catch (error: any) { return { content: [ { type: 'text', text: `āŒ Request Error: ${error.message}`, }, ], }; } }); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`šŸš€ Umbrella MCP Server started successfully (Multi-Tenant)`); console.error(`šŸ“” Base URL: ${this.baseURL}`); console.error(`šŸ”’ Security: Read-only access enabled`); console.error(`šŸ‘„ Multi-tenant: Concurrent user sessions supported`); console.error(`šŸ’” Use "authenticate_user" to get started`); // Set up graceful shutdown process.on('SIGINT', () => { console.error('šŸ›‘ Received SIGINT, shutting down gracefully...'); this.shutdown(); process.exit(0); }); process.on('SIGTERM', () => { console.error('šŸ›‘ Received SIGTERM, shutting down gracefully...'); this.shutdown(); process.exit(0); }); } shutdown(): void { console.error('šŸ”’ Shutting down Umbrella MCP Server...'); this.sessionManager.shutdown(); console.error('āœ… Shutdown complete'); } }

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