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');
}
}