·

Google Cloud MCP With Cursor

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

Connecting GCP MCP to Cursor Agent Mode

Cursor reads MCP config from .cursor/mcp.json at the project root, and the schema is close to Claude Code's but with one difference that matters: Cursor's Agent mode decides on its own, per conversation, whether to invoke a given MCP tool, versus needing an explicit @-mention in some other modes. That autonomy is convenient for flow but means a loosely scoped GCP server gets invoked more readily than you might expect from casual prompting.

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

Enable and verify the servers through Cursor's Settings → MCP panel rather than assuming a valid JSON file means a working connection — Cursor surfaces a green/red status indicator per server there, and a red one with an expandable error is the fastest path to diagnosing a startup failure (usually a bad binary path or missing env var) without digging through logs.

./toolbox --tools-file ./tools.yaml --stdio
gcloud auth application-default print-access-token > /dev/null && echo "ADC OK"

Cursor's Agent mode tool-use is governed by an allow/ask/deny setting per tool, configurable in Settings → MCP → per-server tool permissions. For a BigQuery execute-sql tool, set this to "ask" rather than "always allow" — Cursor will then show a confirmation card with the SQL before running it, which is the IDE-native equivalent of the terminal dry-run discipline covered elsewhere in this module.

Cursor tool permission recommendation:
  execute-sql          → Ask every time
  get_table_schema     → Always allow (read-only, no cost)
  list_datasets        → Always allow (read-only, no cost)
  run.list_services     → Always allow
  run.get_logs           → Always allow
  storage.list_objects   → Always allow
  storage.read_object     → Ask every time (can pull large payloads into context)

This split — auto-allow pure metadata reads, ask before anything that costs money or pulls bulk data — is the single highest-leverage config decision in a Cursor + GCP MCP setup, because it lets exploratory schema-browsing feel instant while keeping a human checkpoint on the two operations (query execution, bulk object reads) that can actually go wrong.

Tips
- Check the Settings → MCP panel's connection status directly rather than trusting that a syntactically valid .cursor/mcp.json means the server started — Cursor's status indicator catches auth and path failures faster than trial-and-error prompting.
- Set per-tool permissions deliberately: auto-allow schema/metadata reads, require confirmation on query execution and bulk object reads.
- ${env:VAR} is Cursor's interpolation syntax for .cursor/mcp.json — verify it resolves correctly (echo the value in a throwaway tool call) rather than assuming parity with Claude Code's ${VAR} syntax carries over cleanly across every field.


Generating Data Pipeline Code Against Real BigQuery Schemas

Where Cursor earns its place in this workflow specifically is generating pipeline code — Python, dbt models, Dataflow templates — that references real BigQuery schemas rather than schemas the model is guessing from a stale internal training snapshot or from partial context you pasted in manually.

Prompt example (in a Python file, Cursor Agent mode):
"Using the gcp-bigquery MCP tools, check the schema of
raw_events.orders and raw_events.order_items, then write a
Python function using the google-cloud-bigquery client library
that loads daily revenue by category into a pandas DataFrame,
parameterized by a date range."

A correct response inspects the schema first, then generates code that matches actual column names and types — this is the difference between generated code that runs on the first try and code with a KeyError on a column name the model half-remembered:

from google.cloud import bigquery
from datetime import date
import pandas as pd

def get_daily_revenue_by_category(
    client: bigquery.Client,
    start_date: date,
    end_date: date,
    project: str = "analytics-project-prod",
) -> pd.DataFrame:
    query = f"""
        SELECT
            DATE(o.order_date) AS day,
            p.category,
            ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
        FROM `{project}.raw_events.orders` AS o
        JOIN `{project}.raw_events.order_items` AS oi
            ON o.order_id = oi.order_id
        JOIN `{project}.raw_events.products` AS p
            ON oi.product_id = p.product_id
        WHERE o.order_date BETWEEN @start_date AND @end_date
            AND o.status = 'completed'
        GROUP BY day, p.category
        ORDER BY day
    """
    job_config = bigquery.QueryJobConfig(
        query_parameters=[
            bigquery.ScalarQueryParameter("start_date", "DATE", start_date),
            bigquery.ScalarQueryParameter("end_date", "DATE", end_date),
        ]
    )
    return client.query(query, job_config=job_config).to_dataframe()

Note the parameterized query (@start_date, @end_date) rather than string-interpolated dates — a well-prompted Cursor session defaults to this pattern for pipeline code specifically (as opposed to one-off interactive queries), because it's generating something meant to run repeatedly with different inputs, not a single ad-hoc analysis.

Push into dbt model generation the same way if that's your stack — the schema-grounding benefit compounds because dbt models reference source tables by name constantly, and a hallucinated column in a dbt model fails at dbt run time, often confusingly:

Prompt example:
"Check the schema for raw_events.sessions, then write a dbt
staging model (models/staging/stg_sessions.sql) that casts
session_date to a proper DATE type and renames device_category
to device_type for consistency with our other staging models."
-- models/staging/stg_sessions.sql
with source as (
    select * from {{ source('raw_events', 'sessions') }}
),

renamed as (
    select
        session_id,
        user_pseudo_id,
        cast(session_date as date) as session_date,
        device_category as device_type,
        traffic_source,
        session_duration_seconds
    from source
)

select * from renamed

Always dry-run the underlying query the generated pipeline code will issue before wiring it into a scheduled job — generated code that's syntactically fine can still carry an expensive query if the WHERE clause doesn't prune partitions the way you intended.

Tips
- Have Cursor inspect the schema before generating pipeline code every time, even for tables you're confident about — schema drift between when you last checked and now is the single most common source of a generated script failing at runtime.
- Prefer parameterized queries (@param placeholders) over f-string interpolation in generated pipeline code — Cursor defaults to this correctly when the prompt frames the code as reusable, but check it explicitly for one-off scripts.
- Dry-run the actual query a generated pipeline will issue, not just review the code — a correct-looking query can still be an expensive one against the real table's partitioning.


Validating Cloud Run Service Config from Cursor

Cursor's IDE context is genuinely useful here because it can cross-reference your actual Dockerfile, service.yaml, or Terraform config against the live deployed Cloud Run service in one conversation — catching drift between what's declared in the repo and what's actually running.

Prompt example:
"Compare the memory and CPU settings in our
infra/cloud-run/checkout-api.yaml against what's actually
deployed for the checkout-api service in us-central1."
resources:
  limits:
    memory: "512Mi"
    cpu: "1"
gcloud run services describe checkout-api --region=us-central1 \
  --format="yaml(spec.template.spec.containers[0].resources)"

This kind of drift — deployed config diverging from the repo's declared config — happens constantly when someone bumps resources manually via the console during an incident and never backports the change to Terraform or the YAML manifest. Cursor catching it mid-conversation, while you're already looking at the service's code, is more likely to actually get fixed than discovering it separately during a quarterly infra audit.

Prompt example:
"That's a real drift — 1Gi/2 CPU deployed vs 512Mi/1 CPU declared.
Update the YAML to match what's actually running, and add a
comment noting this was bumped during the 2026-07-14 incident."

Validate scaling and concurrency settings the same way, since these are just as prone to manual drift and have direct cost implications:

Prompt example:
"Check the min/max instance count and concurrency settings for
checkout-api against infra/cloud-run/checkout-api.yaml. Flag any
difference."
gcloud run services describe checkout-api --region=us-central1 \
  --format="value(spec.template.metadata.annotations['autoscaling.knative.dev/minScale'],
                  spec.template.metadata.annotations['autoscaling.knative.dev/maxScale'],
                  spec.template.spec.containerConcurrency)"

A minScale that got bumped from 0 to 2 during a traffic spike and never reverted is a recurring, quietly expensive drift pattern — always-on instances billing 24/7 for a service that only needs to scale up during business hours. This is exactly the kind of thing worth a periodic Cursor-assisted config audit rather than only checking config when something breaks.

Tips
- Run a config-vs-deployed diff for critical services periodically, not just during incidents — manual console changes during an incident are the most common source of long-lived config drift.
- Pay specific attention to minScale drift — an instance count bumped up during an incident and left there is a recurring, easy-to-miss cost leak.
- Have Cursor annotate the fix with why the config changed (link the incident, the date) when updating repo config to match a deliberate production change — future you needs that context.


Known Limitations and Cost Traps in Cursor

Cursor's Agent mode autonomy is the feature and the risk simultaneously. A few specific traps worth naming from real usage:

Agent mode can chain tool calls further than you expect in one turn. Ask a broad question ("investigate why the analytics dashboard numbers look off") and Cursor's agent may decide on its own to query multiple tables, check several Cloud Run services, and pull logs from three different time windows — all before showing you anything. If any of those intermediate queries are expensive and set to "always allow," you've spent money before seeing a single result. Keep execute-sql on "ask every time" specifically to break this chain at the one point that matters.

Cursor's context window management can drop earlier tool results in long refactor-plus-debug sessions. If you're both writing pipeline code and debugging a live service in the same long session, results from an early BigQuery schema check can fall out of context by the time you're generating code twenty turns later — leading to code that references a slightly wrong column name from an earlier, now-forgotten correction. Re-verify schema assumptions in long sessions rather than trusting early-session grounding held throughout.

No built-in per-project cost dashboard inside Cursor. Unlike GitHub Copilot's usage metering for AI requests, Cursor has no visibility into your GCP spend — that stays entirely in GCP's own billing console and budget alerts (covered in this module's first topic). Don't expect Cursor to warn you as your cumulative BigQuery costs from a session climb; only individual dry-run confirmations do that, and only if you've kept execute-sql on "ask."

Community GCP MCP servers used with Cursor have had auth-refresh bugs. A few community servers (not genai-toolbox, which handles this correctly) cache a short-lived ADC token at startup and don't refresh it, causing tool calls to fail with an opaque 401 after roughly an hour of a long Cursor session. If a previously-working GCP MCP tool starts failing mid-session, restarting the MCP server (toggle it off/on in Settings → MCP) is the fast fix; filing that as a bug against the specific server is the durable one.

Generated pipeline code still needs a human review pass for cost-sensitive scheduling decisions. Cursor will happily generate a Cloud Scheduler + Cloud Run job that re-runs a moderately expensive query every 5 minutes if you ask for "frequent" refreshes without specifying an interval — it optimizes for matching your literal request, not for guessing an appropriate cost/freshness tradeoff you didn't specify.

Tips
- Keep execute-sql (and any other cost-incurring tool) on "ask every time" specifically to prevent Agent mode from chaining several expensive calls before you see any output.
- Re-verify schema assumptions explicitly in long, multi-topic sessions rather than trusting a schema check from many turns earlier is still in context.
- If a GCP MCP tool call fails with an unexplained 401 partway through a long session, restart the MCP server before assuming a permissions problem — it's often a stale cached token in the server itself.


Tips

Tips
- Configure per-tool permissions deliberately in Settings → MCP the first time you connect a GCP server — this single setting does more to prevent runaway cost than any prompting discipline.
- Use Cursor specifically for the schema-grounded code generation use case (pipelines, dbt models) where it has a clear edge from tight IDE + repo context integration.
- Pair Cursor's config-drift-detection strength with a recurring habit (monthly, or after any incident involving manual console changes) rather than only checking when something visibly breaks.