GTM engineeringTutorial

Connecting Search Console to an AI agent when your org blocks service-account keys

The three errors you hit wiring Search Console to an MCP server, and the ADC path around them. With the exact API responses.

What we ranThe three exact API error payloads (FAILED_PRECONDITION on key creation, SERVICE_DISABLED without a quota project, 403 on sitemap submit) captured while wiring this up on 2026-07-20, plus the working ADC scope string.

Most guides for connecting Google Search Console to an MCP server tell you to create a service account, download a JSON key, and point GOOGLE_APPLICATION_CREDENTIALS at it. On a Google Cloud org with any security posture, step two fails. Here is what actually comes back, and the path that works without weakening your org policy.

The key-creation wall

Creating the service account succeeds. Creating its key does not:

gcloud iam service-accounts keys create ./sa-key.json \
  [email protected]
ERROR: (gcloud.iam.service-accounts.keys.create) FAILED_PRECONDITION:
Key creation is not allowed on this service account.
- '@type': type.googleapis.com/google.rpc.PreconditionFailure
  violations:
  - description: Key creation is not allowed on this service account.
    type: constraints/iam.disableServiceAccountKeyCreation

constraints/iam.disableServiceAccountKeyCreation is a common org-level guardrail, and it exists for a good reason: a downloaded key is a long-lived credential that lands in someone's home directory and stays there. You can override it per-project, but that means enabling the Org Policy API and turning off a security control for a read-only reporting integration. Bad trade.

Use application default credentials instead

Search Console's API cares about who is authorised on the property, and a human account already is. google-auth-library — which most MCP servers for this use — accepts an authorized_user credential in the same keyFile slot as a service-account key, so nothing on the server side has to change:

gcloud auth application-default login \
  --scopes=https://www.googleapis.com/auth/webmasters.readonly,\
https://www.googleapis.com/auth/cloud-platform,openid,\
https://www.googleapis.com/auth/userinfo.email

The scopes matter. For a user credential the scopes granted at consent time are the ones you get — passing scopes to GoogleAuth later is silently ignored, so if you omit webmasters.readonly here you will get an opaque permission error at call time with nothing pointing back at this command.

Point the server at the resulting file:

GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json

The quota-project error

Authentication now works and the API still refuses you:

{
  "error": {
    "code": 403,
    "message": "Your application is authenticating by using local Application Default Credentials. The searchconsole.googleapis.com API requires a quota project, which is not set by default.",
    "status": "PERMISSION_DENIED",
    "details": [{ "reason": "SERVICE_DISABLED" }]
  }
}

User credentials have no project to bill quota against, so the API wants one named explicitly. gcloud auth application-default login writes quota_project_id into the credentials file, and google-auth-library reads it and sends the header for you — which is why the MCP server works while your curl reproduction does not. For raw calls you have to add it yourself:

TOKEN=$(gcloud auth application-default print-access-token)
curl -s \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-goog-user-project: my-project" \
  https://www.googleapis.com/webmasters/v3/sites
{
  "siteEntry": [
    { "siteUrl": "sc-domain:example.com", "permissionLevel": "siteOwner" }
  ]
}

Also enable the API on that project first — gcloud services enable searchconsole.googleapis.com — or the same call fails with SERVICE_DISABLED and a message about the quota project, which sends you back down the wrong path.

What read-only actually costs you

webmasters.readonly covers everything an agent needs to analyse: performance data, URL inspection, listing sitemaps. It does not cover submitting one:

curl -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "x-goog-user-project: my-project" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps/https%3A%2F%2Fexample.com%2Fsitemap.xml"
# 403

Swapping to the read-write webmasters scope fixes it. We deliberately did not. An agent that reads your search data and an agent that can mutate your Search Console configuration are different risk profiles, and sitemap submission is a once-per-site action — not worth handing over write access to automate. If you are wiring an agent that runs unattended, read-only is the right default and the 403 is the feature.

Caveats worth knowing before you copy this

The credential is tied to a human account, which has two consequences. It lives on one machine, so a cloud job cannot use it — for unattended remote runs you are back to needing a service-account key, and therefore back to the org-policy conversation. And it inherits that person's access: if they leave or lose the property, the integration dies with no warning beyond a sudden permission error.

For a laptop-side agent reading its own site's search data, that is a fair trade. For anything shared or scheduled in the cloud, plan on the key.

Verified against the Search Console API on 2026-07-20. Error payloads are real responses, lightly redacted for project and domain names.