·

Google Cloud MCP With Claude Code CLI and VS Code

Set up Google Cloud MCP in Claude Code CLI and VS Code so your AI agent can manage cloud resources and investigate infrastructure right from your editor.

Installing and Connecting GCP MCP to Claude Code

Claude Code reads MCP server definitions from .mcp.json at the project root (shared with your team via git) or from user-scoped config for personal servers. For GCP, I recommend the project-scoped route once a server is validated, since it lets the whole team share the same tool surface and role scoping.

Install the server first — Google's genai-toolbox is the most robust option for BigQuery-heavy workflows as of early 2026:

curl -O https://storage.googleapis.com/genai-toolbox/v0.6.0/darwin/arm64/toolbox
chmod +x toolbox
./toolbox --version

Configure tools.yaml to define your BigQuery data sources explicitly rather than granting free-form database access:

sources:
  analytics-bq:
    kind: bigquery
    project: analytics-project-prod
    location: US

tools:
  execute-sql:
    kind: bigquery-sql
    source: analytics-bq
    description: Execute a read-only SQL query against BigQuery.

Then wire it into Claude Code's .mcp.json:

{
  "mcpServers": {
    "gcp-bigquery": {
      "command": "./toolbox",
      "args": ["--tools-file", "./tools.yaml", "--stdio"],
      "env": {
        "GOOGLE_APPLICATION_CREDENTIALS": "${HOME}/.gcp/mcp-agent-key.json"
      }
    }
  }
}

For Cloud Run and GCS coverage, add a second server — many teams run a lighter community gcp-mcp wrapper alongside genai-toolbox rather than waiting for one project to cover everything:

{
  "mcpServers": {
    "gcp-bigquery": {
      "command": "./toolbox",
      "args": ["--tools-file", "./tools.yaml", "--stdio"]
    },
    "gcp-run-logs": {
      "command": "npx",
      "args": ["-y", "gcp-mcp-server", "--services", "run,logging,storage"],
      "env": {
        "GOOGLE_CLOUD_PROJECT": "analytics-project-prod",
        "GOOGLE_APPLICATION_CREDENTIALS": "${HOME}/.gcp/mcp-agent-key.json"
      }
    }
  }
}

Verify the connection inside Claude Code with /mcp — it lists connected servers, their tool counts, and connection health. If a server shows as failed, run it standalone first (./toolbox --tools-file ./tools.yaml --stdio in a terminal) to see the raw error before debugging through Claude Code's wrapper, which often swallows stderr detail.

For VS Code with the Claude Code extension, the same .mcp.json at the workspace root is picked up automatically — there's no separate VS Code-specific MCP config for Claude Code specifically, which is one less thing to keep in sync compared to juggling Cursor's .cursor/mcp.json format.

gcloud auth application-default print-access-token > /dev/null && echo "ADC OK"
bq ls --project_id=analytics-project-prod

Tips
- Keep tools.yaml in version control but keep the service account key path as an env var reference, never a literal path baked into a committed file.
- Run toolbox --tools-file ./tools.yaml --stdio manually once after any config change — catching a YAML typo in a terminal is faster than debugging it through three layers of MCP indirection.
- If your org uses multiple GCP projects, define one tools.yaml per project and name the MCP server entries accordingly (gcp-bigquery-staging, gcp-bigquery-prod) so Claude Code's tool names make the target obvious.


Writing and Cost-Estimating BigQuery SQL with AI Before Running It

The workflow that actually earns its keep here is: describe the question in plain English, let Claude Code inspect the real schema, get a dry-run cost estimate, review it, then execute. Skipping the dry-run step is the single most common way teams get an unpleasant BigQuery bill surprise from an AI-assisted session.

Prompt example:
"Look at the schema for analytics-project-prod.raw_events.page_views
and write a query for weekly unique visitors by traffic_source for
Q1 2026. Dry-run it first and tell me the bytes scanned before
running it for real."

A well-configured execute-sql tool in genai-toolbox supports a dryRun parameter, and a disciplined agent will call it automatically when a query is not schema-inspection-only, but you should also verify this from the terminal side so you're not solely trusting the model's judgment:

bq query --use_legacy_sql=false --dry_run \
  'SELECT
     traffic_source,
     DATE_TRUNC(event_date, WEEK) AS week,
     COUNT(DISTINCT user_pseudo_id) AS unique_visitors
   FROM `analytics-project-prod.raw_events.page_views`
   WHERE event_date BETWEEN "2026-01-01" AND "2026-03-31"
   GROUP BY traffic_source, week'

4.8 GB scanned against the on-demand pricing tier (roughly $6.25/TB as of 2026 pricing) is about $0.03 — trivial. The real risk shows up when someone forgets a WHERE clause on a partitioned table and the dry run reports 40 TB instead. That's the moment the dry-run step justifies itself; catching it before execution, not after the invoice.

Prompt example for cost-sensitive query building:
"Before writing this query, check if raw_events.page_views is
partitioned or clustered, and make sure the query prunes to the
Q1 2026 partitions only. Show me the dry-run byte estimate and
flag it if it's above 5 GB."

Ask Claude Code to check partitioning explicitly — bigquery.get_table_schema responses from most MCP servers include partitioning metadata, but the model won't always surface it unprompted unless the prompt specifically asks for a partition-pruning check.

-- Confirming partition field via INFORMATION_SCHEMA
SELECT table_name, ddl
FROM `analytics-project-prod.raw_events.INFORMATION_SCHEMA.TABLES`
WHERE table_name = 'page_views';

Enforce a hard ceiling regardless of what the dry run reports, so a misread estimate can't turn into a real bill:

bq query --use_legacy_sql=false \
  --maximum_bytes_billed=5000000000 \
  'SELECT traffic_source, COUNT(DISTINCT user_pseudo_id) AS uv
   FROM `analytics-project-prod.raw_events.page_views`
   WHERE event_date BETWEEN "2026-01-01" AND "2026-03-31"
   GROUP BY traffic_source'

Tips
- Treat any dry-run estimate over 10 GB on a table you expected to be small as a signal the query isn't pruning partitions — investigate before running, don't just raise the byte cap.
- Ask for EXPLAIN-style reasoning in the prompt ("explain why this query needs to scan the full table") when a dry-run number surprises you — the agent can usually diagnose a missing partition filter correctly.
- Keep a running log of dry-run estimates for recurring queries in a scratch file — it makes cost regressions (a schema change that removes clustering, say) visible over time instead of buried in monthly billing exports.


Inspecting Cloud Run Revisions, Traffic Splits, and Logs from the Terminal

Cloud Run debugging is where the MCP-assisted terminal workflow beats the console UI decisively — correlating a revision's config with its logs in the console means five separate page loads; through Claude Code it's two or three tool calls in one conversation.

Start by listing revisions and traffic allocation directly, both as a manual sanity check and to give the agent ground truth to compare its tool output against:

gcloud run services describe checkout-api --region=us-central1 --format=json | \
  jq '.status.traffic'

Prompt example:
"checkout-api-00048-abc is the canary revision at 10% traffic.
Pull the last 30 minutes of ERROR and CRITICAL logs for that
revision specifically (not the whole service) and tell me if
the error rate looks different from the 00047 baseline revision."

A revision-scoped log filter is what makes this precise instead of noisy — without it, you get logs from every revision serving the service, which is useless when you're specifically trying to validate a canary:

gcloud logging read \
  'resource.type="cloud_run_revision"
   AND resource.labels.service_name="checkout-api"
   AND resource.labels.revision_name="checkout-api-00048-abc"
   AND severity>=ERROR' \
  --freshness=30m --format=json --limit=100

For deploy-failure debugging specifically, the most useful diagnostic is diffing the failing revision's environment and resource config against the last known-good one:

gcloud run revisions describe checkout-api-00048-abc --region=us-central1 \
  --format="yaml(spec.containers[0].env, spec.containers[0].resources)" \
  > /tmp/rev-00048.yaml

gcloud run revisions describe checkout-api-00047-xyz --region=us-central1 \
  --format="yaml(spec.containers[0].env, spec.containers[0].resources)" \
  > /tmp/rev-00047.yaml

diff /tmp/rev-00047.yaml /tmp/rev-00048.yaml
Prompt example:
"Compare the env vars and resource limits between revision
00047 and 00048 of checkout-api. The new one is crash-looping —
check if a required env var is missing or a memory limit got
reduced below what the process needs at startup."

In real incidents this pattern catches the two most common Cloud Run deploy failures fast: a missing secret reference (someone forgot to add DATABASE_URL to the new revision's env, so Secret Manager silently returns nothing instead of erroring loudly) and a memory limit reduction that wasn't validated against actual startup memory usage.

Tips
- Always scope log queries to a specific revision_name, not just service_name, when multiple revisions are live — traffic-split debugging without this filter is close to useless.
- Ask the agent to compare configs before logs, not after — a config diff often explains the failure directly and saves a log-reading pass entirely.
- --freshness in gcloud logging read is relative to "now," which drifts if you're investigating an incident from hours ago — pass explicit timestamp>="2026-08-20T14:00:00Z" filters instead for anything not actively happening right now.


Prompting Patterns for Safe, Bounded Data Exploration

The failure mode to design against isn't malice, it's scope creep — an agent asked to "look into the orders data" that decides to scan three additional tables it thought were relevant. Bounded prompts prevent this more reliably than hoping the model exercises restraint on its own.

Three patterns that consistently keep exploration safe and cheap:

Name the exact table and columns up front. Don't say "look at our orders data" — say "query analytics-project-prod.raw_events.orders, columns order_id, region, created_at, total_amount only." This eliminates the schema-guessing step where the model sometimes queries INFORMATION_SCHEMA across an entire dataset just to find the table it wants.

Weak prompt:
"Find out why revenue looks low this month."

Bounded prompt:
"Query analytics-project-prod.raw_events.orders (columns:
order_id, region, created_at, total_amount, status) for
August 2026. Compare total_amount summed by week against
the same weeks in July 2026. Dry-run first; only run if
under 2 GB scanned."

Set an explicit byte or row ceiling in the prompt, and ask the agent to state it before executing. This works because Claude Code will actually echo the constraint back and check it against the dry-run result if you ask for that confirmation explicitly — it's a cheap habit that catches runaway scans before they happen.

Prompt example:
"Before running any query, dry-run it and tell me the bytes
scanned. If it's over 2 GB, stop and ask me before proceeding."

Time-box log and GCS exploration the same way you time-box SQL. "List objects in gs://prod-exports/" against a bucket with two million objects will either time out or return a response too large to be useful. Scope by prefix and limit explicitly.

Prompt example:
"List objects in gs://prod-exports/2026/08/ with prefix
'daily-report-' — limit to 20 results, sorted by most recent."

Combine these into a standing instruction if your team runs GCP MCP regularly — a project-level CLAUDE.md note that says "always dry-run BigQuery before executing; always scope Cloud Logging queries to a specific resource and time window; never list a GCS bucket without a prefix" removes the need to repeat these constraints in every prompt.

Tips
- Put your bounding rules in the project's CLAUDE.md (or equivalent agent instructions file) once, rather than retyping them per session — consistency matters more here than cleverness.
- When an agent's first move is to explore broadly ("let me check what tables exist"), that's a signal your prompt under-specified the target — tighten it rather than letting the exploration run.
- Review the actual tool calls Claude Code made (visible in its transcript) after a session touching BigQuery, at least until you've built confidence the bounding prompts are being honored consistently.


Tips

Tips
- Validate .mcp.json server connections with /mcp at the start of every session touching GCP — a silently disconnected server means the agent falls back to guessing, which is worse than an explicit error.
- Keep BigQuery-writing prompts and BigQuery-reading prompts in separate sessions when possible — it's easier to review a transcript for correctness when it isn't mixed with unrelated exploration.
- Revisit your tools.yaml and service account roles quarterly — schemas and team access needs drift, and a scoping decision that made sense six months ago often doesn't match current usage.