gcp-billing-service-breakdown
Analyze Google Cloud costs by service with usage and SKU details to identify spending patterns and optimize resource allocation.
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:1081-1248 (handler)The handler function for 'gcp-billing-service-breakdown' tool. It generates a detailed mock cost breakdown by Google Cloud service, including project, SKU, cost, usage, labels, grouped by service with tables and summary statistics.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 for the tool: billingAccountName (required string), projectId (optional string), timeRange (enum with 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)"), }, },
- src/services/billing/tools.ts:1060-1249 (registration)Registration of the 'gcp-billing-service-breakdown' tool using server.registerTool, including title, description, inputSchema, and handler function.{ 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, ); } }, );
- Helper function formatCurrency used in the handler to format monetary amounts for display in the response.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); }
- TypeScript interface CostData used to type the mock cost data structures in the handler.export interface CostData { billingAccountName: string; projectId?: string; serviceId?: string; skuId?: string; cost: { amount: number; currency: string; }; usage: { amount: number; unit: string; }; period: { startTime: string; endTime: string; }; labels?: Record<string, string>; }