gcp-billing-service-breakdown
Analyze Google Cloud costs by service, usage, and SKU for a specified billing account. Filter by project ID and time range to gain detailed expense insights.
Instructions
Get detailed cost breakdown by Google Cloud service with usage and SKU information
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| billingAccountName | Yes | Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012') | |
| projectId | No | Optional project ID to filter costs | |
| timeRange | No | Time range for analysis (7d, 30d, 90d, 1y) | 30d |
Implementation Reference
- src/services/billing/tools.ts:1059-1249 (registration)Complete registration of the 'gcp-billing-service-breakdown' MCP tool, including input schema validation with Zod, title, description, and the full async handler function that generates mock detailed service cost breakdowns grouped by service with SKU, usage, labels, and summary statistics in markdown format."gcp-billing-service-breakdown", { title: "Get Service Cost Breakdown", description: "Get detailed cost breakdown by Google Cloud service with usage and SKU information", inputSchema: { billingAccountName: z .string() .describe( "Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012')", ), projectId: z .string() .optional() .describe("Optional project ID to filter costs"), timeRange: z .enum(["7d", "30d", "90d", "1y"]) .default("30d") .describe("Time range for analysis (7d, 30d, 90d, 1y)"), }, }, async ({ billingAccountName, projectId, timeRange }) => { try { // Use project hierarchy: provided -> state manager -> auth default const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); logger.debug( `Getting service breakdown for billing account: ${billingAccountName}, project: ${actualProjectId || "all"}, range: ${timeRange}`, ); // Mock detailed service cost data const serviceBreakdown: CostData[] = [ { billingAccountName, projectId: actualProjectId || "production-project", serviceId: "compute.googleapis.com", skuId: "services/6F81-5844-456A/skus/CP-COMPUTEENGINE-VMIMAGE-N1-STANDARD-1", cost: { amount: 1847.25, currency: "USD" }, usage: { amount: 744, unit: "hours" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "production", team: "backend", instance_type: "n1-standard-1", zone: "us-central1-a", }, }, { billingAccountName, projectId: actualProjectId || "production-project", serviceId: "storage.googleapis.com", skuId: "services/95FF-2EF5-5EA1/skus/9E26-D0CA-7C08", cost: { amount: 426.8, currency: "USD" }, usage: { amount: 2500, unit: "GB" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "production", team: "data", storage_class: "standard", location: "us-central1", }, }, { billingAccountName, projectId: actualProjectId || "development-project", serviceId: "bigquery.googleapis.com", skuId: "services/24E6-581D-38E5/skus/1145-49C5-8000", cost: { amount: 189.45, currency: "USD" }, usage: { amount: 1250, unit: "TB" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "development", team: "analytics", query_type: "on_demand", }, }, ]; let response = `# Service Cost Breakdown\n\n`; response += `**Billing Account:** ${billingAccountName}\n`; response += `**Project:** ${actualProjectId || "All projects"}\n`; response += `**Time Range:** ${timeRange}\n`; response += `**Services Analysed:** ${new Set(serviceBreakdown.map((c) => c.serviceId)).size}\n\n`; response += `⚠️ **Note:** This is demonstration data with detailed service and SKU breakdown.\n\n`; // Group by service for detailed analysis const serviceMap = new Map<string, CostData[]>(); serviceBreakdown.forEach((costData: CostData) => { if (!serviceMap.has(costData.serviceId!)) { serviceMap.set(costData.serviceId!, []); } serviceMap.get(costData.serviceId!)!.push(costData); }); // Detailed breakdown by service for (const [serviceId, costs] of serviceMap) { const serviceName = serviceId .replace(".googleapis.com", "") .toUpperCase(); const serviceTotal = costs.reduce( (sum, cost) => sum + cost.cost.amount, 0, ); response += `## ${serviceName}\n\n`; response += `**Service ID:** ${serviceId}\n`; response += `**Total Cost:** ${formatCurrency(serviceTotal)}\n`; response += `**SKUs:** ${costs.length}\n\n`; response += "| Project | SKU ID | Cost | Usage | Labels |\n"; response += "|---------|--------|------|-------|--------|\n"; costs.forEach((costData: CostData) => { const project = costData.projectId || "Unknown"; const skuId = costData.skuId ? costData.skuId.split("/").pop() : "Unknown"; const cost = formatCurrency(costData.cost.amount); const usage = `${costData.usage.amount} ${costData.usage.unit}`; const labels = costData.labels ? Object.entries(costData.labels) .map(([k, v]) => `${k}:${v}`) .join(", ") : "None"; response += `| ${project} | ${skuId} | ${cost} | ${usage} | ${labels} |\n`; }); response += "\n"; } // Summary const totalCost = serviceBreakdown.reduce( (sum, cost) => sum + cost.cost.amount, 0, ); response += `## Summary\n\n`; response += `**Total Cost:** ${formatCurrency(totalCost)}\n`; response += `**Average Cost per Service:** ${formatCurrency(totalCost / serviceMap.size)}\n`; // Top cost drivers const sortedServices = Array.from(serviceMap.entries()) .map(([serviceId, costs]) => ({ serviceId, total: costs.reduce((sum, cost) => sum + cost.cost.amount, 0), })) .sort((a, b) => b.total - a.total); response += `**Top Cost Driver:** ${sortedServices[0].serviceId} (${formatCurrency(sortedServices[0].total)})\n`; return { content: [ { type: "text", text: response, }, ], }; } catch (error: any) { logger.error(`Error getting service breakdown: ${error.message}`); throw new GcpMcpError( `Failed to get service breakdown: ${error.message}`, error.code || "UNKNOWN", error.status || 500, ); } }, );
- src/services/billing/tools.ts:1080-1248 (handler)The core handler logic for executing the tool. Uses mock data to simulate GCP billing service breakdown, processes into per-service tables showing projects, SKUs, costs, usage, and labels. Generates comprehensive markdown report with summaries and top cost drivers.async ({ billingAccountName, projectId, timeRange }) => { try { // Use project hierarchy: provided -> state manager -> auth default const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); logger.debug( `Getting service breakdown for billing account: ${billingAccountName}, project: ${actualProjectId || "all"}, range: ${timeRange}`, ); // Mock detailed service cost data const serviceBreakdown: CostData[] = [ { billingAccountName, projectId: actualProjectId || "production-project", serviceId: "compute.googleapis.com", skuId: "services/6F81-5844-456A/skus/CP-COMPUTEENGINE-VMIMAGE-N1-STANDARD-1", cost: { amount: 1847.25, currency: "USD" }, usage: { amount: 744, unit: "hours" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "production", team: "backend", instance_type: "n1-standard-1", zone: "us-central1-a", }, }, { billingAccountName, projectId: actualProjectId || "production-project", serviceId: "storage.googleapis.com", skuId: "services/95FF-2EF5-5EA1/skus/9E26-D0CA-7C08", cost: { amount: 426.8, currency: "USD" }, usage: { amount: 2500, unit: "GB" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "production", team: "data", storage_class: "standard", location: "us-central1", }, }, { billingAccountName, projectId: actualProjectId || "development-project", serviceId: "bigquery.googleapis.com", skuId: "services/24E6-581D-38E5/skus/1145-49C5-8000", cost: { amount: 189.45, currency: "USD" }, usage: { amount: 1250, unit: "TB" }, period: { startTime: new Date( Date.now() - parseInt(timeRange.slice(0, -1)) * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, labels: { environment: "development", team: "analytics", query_type: "on_demand", }, }, ]; let response = `# Service Cost Breakdown\n\n`; response += `**Billing Account:** ${billingAccountName}\n`; response += `**Project:** ${actualProjectId || "All projects"}\n`; response += `**Time Range:** ${timeRange}\n`; response += `**Services Analysed:** ${new Set(serviceBreakdown.map((c) => c.serviceId)).size}\n\n`; response += `⚠️ **Note:** This is demonstration data with detailed service and SKU breakdown.\n\n`; // Group by service for detailed analysis const serviceMap = new Map<string, CostData[]>(); serviceBreakdown.forEach((costData: CostData) => { if (!serviceMap.has(costData.serviceId!)) { serviceMap.set(costData.serviceId!, []); } serviceMap.get(costData.serviceId!)!.push(costData); }); // Detailed breakdown by service for (const [serviceId, costs] of serviceMap) { const serviceName = serviceId .replace(".googleapis.com", "") .toUpperCase(); const serviceTotal = costs.reduce( (sum, cost) => sum + cost.cost.amount, 0, ); response += `## ${serviceName}\n\n`; response += `**Service ID:** ${serviceId}\n`; response += `**Total Cost:** ${formatCurrency(serviceTotal)}\n`; response += `**SKUs:** ${costs.length}\n\n`; response += "| Project | SKU ID | Cost | Usage | Labels |\n"; response += "|---------|--------|------|-------|--------|\n"; costs.forEach((costData: CostData) => { const project = costData.projectId || "Unknown"; const skuId = costData.skuId ? costData.skuId.split("/").pop() : "Unknown"; const cost = formatCurrency(costData.cost.amount); const usage = `${costData.usage.amount} ${costData.usage.unit}`; const labels = costData.labels ? Object.entries(costData.labels) .map(([k, v]) => `${k}:${v}`) .join(", ") : "None"; response += `| ${project} | ${skuId} | ${cost} | ${usage} | ${labels} |\n`; }); response += "\n"; } // Summary const totalCost = serviceBreakdown.reduce( (sum, cost) => sum + cost.cost.amount, 0, ); response += `## Summary\n\n`; response += `**Total Cost:** ${formatCurrency(totalCost)}\n`; response += `**Average Cost per Service:** ${formatCurrency(totalCost / serviceMap.size)}\n`; // Top cost drivers const sortedServices = Array.from(serviceMap.entries()) .map(([serviceId, costs]) => ({ serviceId, total: costs.reduce((sum, cost) => sum + cost.cost.amount, 0), })) .sort((a, b) => b.total - a.total); response += `**Top Cost Driver:** ${sortedServices[0].serviceId} (${formatCurrency(sortedServices[0].total)})\n`; return { content: [ { type: "text", text: response, }, ], }; } catch (error: any) { logger.error(`Error getting service breakdown: ${error.message}`); throw new GcpMcpError( `Failed to get service breakdown: ${error.message}`, error.code || "UNKNOWN", error.status || 500, ); } },
- Zod input schema defining parameters: required billingAccountName (string), optional projectId (string), timeRange (enum ["7d","30d","90d","1y"], default "30d").billingAccountName: z .string() .describe( "Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012')", ), projectId: z .string() .optional() .describe("Optional project ID to filter costs"), timeRange: z .enum(["7d", "30d", "90d", "1y"]) .default("30d") .describe("Time range for analysis (7d, 30d, 90d, 1y)"), },
- formatCurrency utility function imported and used multiple times in the handler to format cost amounts (e.g., serviceTotal, totalCost) for display in the markdown response tables and summaries.export function formatCurrency( amount: number, currency: string = "USD", ): string { return new Intl.NumberFormat("en-AU", { style: "currency", currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 4, }).format(amount); }