GCP has no "get my spend" button
Unlike a single dashboard call, real cost-by-service data lives in a BigQuery billing export table, and cloud waste hides across three completely different APIs. FinOps work is mostly knowing where to look.
Unattached disks
Persistent disks left behind after a VM is deleted keep billing indefinitely, found via Compute Engine's Disk.users field.
Idle VMs
Running instances doing nothing. Confirming "idle" (not just "running") means cross-referencing Cloud Monitoring's 7-day average CPU.
Unused static IPs
A reserved external IP bills hourly the moment it's no longer attached to anything, easy to forget, easy to detect.
Two MCP tools, one server
server.py exposes both over stdio. Each tries the real GCP API first, then degrades to clearly labeled mock data, so a demo (or a misconfigured production run) still returns something useful instead of crashing.
get_gcp_monthly_cost_summary()
Queries the BigQuery billing export for the current month, grouped by service. Falls back to a realistic mock breakdown when credentials or the export table aren't configured.
detect_idle_gcp_resources()
Scans Compute Engine for unattached disks and unused static IPs, and cross-checks running instances against Cloud Monitoring CPU data to flag idle VMs.
Inside one real run
This is an actual trace captured while testing the agent: python agent.py "Check my GCP cost and find any waste", running on Gemini's free tier. Click a stage to see what happened inside it.
Not a mockup: two real runs
Unedited terminal output from python agent.py, on Gemini's free tier, no GCP credentials configured. Both tools returned their labeled mock data, and the LLM still had to read it, pick the right numbers, and answer correctly.
$ python agent.py "Check my GCP cost and find any waste"

$ python agent.py "What's my biggest cost driver this month?"

Diagrams
The same system, two views: the request flow over time, and the architecture at rest.
Sequence: one request, two tool calls
agent.py orchestrates; server.py stays a dumb, stateless tool provider.
sequenceDiagram
actor User
participant Agent as agent.py (MCP client)
participant LLM as LLM (Gemini or Claude)
participant Server as server.py (MCP server)
participant GCP as GCP APIs / mock fallback
User->>Agent: "Check my GCP cost and find any waste"
Agent->>Server: list_tools()
Server-->>Agent: [get_gcp_monthly_cost_summary, detect_idle_gcp_resources]
Agent->>LLM: prompt + tool schemas
LLM-->>Agent: tool_use: get_gcp_monthly_cost_summary()
Agent->>Server: call_tool(get_gcp_monthly_cost_summary)
Server->>GCP: BigQuery billing export query
GCP-->>Server: cost breakdown by service
Server-->>Agent: tool result (JSON)
Agent->>LLM: tool result
LLM-->>Agent: tool_use: detect_idle_gcp_resources()
Agent->>Server: call_tool(detect_idle_gcp_resources)
Server->>GCP: Compute Engine + Cloud Monitoring scan
GCP-->>Server: idle disks / VMs / static IPs
Server-->>Agent: tool result (JSON)
Agent->>LLM: tool result
LLM-->>Agent: final synthesized answer
Agent-->>User: cost summary + waste report
Architecture: at rest
Every real data source has a labeled mock fallback right behind it.
flowchart LR
subgraph ClientApp["agent.py (MCP client)"]
Loop["LLM Tool-Use Loop\n(Gemini or Claude)"]
end
ClientApp <-->|"JSON-RPC 2.0 over stdio"| Server["server.py (MCP server)"]
Server --> Cost["get_gcp_monthly_cost_summary()"]
Server --> Idle["detect_idle_gcp_resources()"]
Cost <--> BQ[("BigQuery Billing Export")]
Idle <--> CE[("Compute Engine + Cloud Monitoring")]
BQ -. fallback .-> Mock1[("Mock cost data")]
CE -. fallback .-> Mock2[("Mock idle-resource data")]