Skip to content

feat(google-analytics-sa): GA4 MCP with service account auth - #550

Merged
JonasJesus42 merged 2 commits into
mainfrom
ga-mcp-service-account-auth
Aug 24, 2026
Merged

feat(google-analytics-sa): GA4 MCP with service account auth#550
JonasJesus42 merged 2 commits into
mainfrom
ga-mcp-service-account-auth

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds google-analytics-sa, a server-to-server variant of the Google Analytics MCP that reuses the eight existing GA tools verbatim (via new subpath exports) and swaps the OAuth bearer for a service account token — no per-user login, no refresh token to keep alive. GA4 grants access to the service account identity directly, so unlike google-calendar-sa there is no domain-wide delegation, no impersonation list and no fan-out; credentials resolve in one place, preferring a key pasted into the connection's SERVICE_ACCOUNT_JSON and falling back to a managed key from the GOOGLE_SERVICE_ACCOUNT_JSON site secret, so a customer can either keep quota in their own GCP project or just grant a Viewer role and skip key handling. A check-service-account-access tool makes setup self-service: it reports which credential is active, which email to grant, which properties are readable, and the exact next step when something is missing — returning failures as data rather than throwing, so a misconfigured install always gets an actionable answer.

The JWT-bearer implementation moves out of google-calendar-sa into @decocms/mcps-shared/google-service-account, which fixes three bugs on the way: subject becomes optional (the sub claim must be absent for GA4), the token cache key now includes client_email (two connections with different service accounts impersonating the same user previously shared a token), and the calendar fan-out no longer mints per-email tokens into a single shared MESH_REQUEST_CONTEXT.authorization inside a Promise.all, which made list_events across two users return one user's calendar duplicated. The new app wraps its transport in withAuth and is not added to auth-exemptions.json; 14 tests cover the JWT claims, cache isolation, error propagation and credential precedence, and all four check-service-account-access failure paths were exercised against a running server.

Deploying requires setting the GOOGLE_SERVICE_ACCOUNT_JSON secret on the google-analytics-sa site — without it only the bring-your-own-key mode works. The happy path (ok: true with properties listed) has not been verified against a real GA4 property.

🤖 Generated with Claude Code


Summary by cubic

Adds google-analytics-sa, a GA4 MCP that uses a service account instead of OAuth, removing per-user login and refresh token handling while keeping the same reporting tools. Also extracts JWT-bearer auth to @decocms/mcps-shared/google-service-account and fixes cache and fan-out bugs in google-calendar-sa.

  • Reuses the eight google-analytics tools via new subpath exports; tools read the bearer from MESH_REQUEST_CONTEXT and run with a cloned env to avoid shared mutation.
  • New check-service-account-access tool reports active credential source, service account email to grant, readable properties, and next steps; returns failures as data.
  • @decocms/mcps-shared/google-service-account: makes subject optional and last; cache key now includes client_email; uses WebCrypto only; clearer key parsing errors (detects OAuth client files).
  • google-calendar-sa now clones env per impersonated user and stops writing tokens into a shared authorization during fan-out.
  • google-analytics adds exports (./tools, ./constants, ./client, ./schemas, ./types) and marks GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET optional for service-account builds.
  • Transport is wrapped with withAuth; app is added to deploy.json and registry.json. Tests cover JWT claims, cache isolation, env cloning, error propagation, and credential precedence.

Bolded section: Rollout and migration

  • Set the GOOGLE_SERVICE_ACCOUNT_JSON site secret on the google-analytics-sa site to enable managed credentials, or paste a key into the connection’s SERVICE_ACCOUNT_JSON.
  • Grant the reported service account email Viewer access on the GA4 property, then run check-service-account-access to confirm.
  • Update any internal calls to getServiceAccountAccessToken to the new signature: (serviceAccountJson, scopes, subject?).

Written for commit 42cc9bd. Summary will update on new commits.

Review in cubic

JonasJesus42 and others added 2 commits August 24, 2026 14:56
…elper

Moves google-calendar-sa's JWT-bearer implementation to
@decocms/mcps-shared/google-service-account so a second SA-backed MCP can
reuse it, and fixes three problems along the way:

- `subject` is now optional and last. It is only needed for domain-wide
  delegation; APIs that grant access to the service account identity
  directly (GA4) must omit the `sub` claim entirely.
- The token cache key now includes `client_email`. It was keyed on
  subject + scopes alone, so two connections using different service
  accounts to impersonate the same user shared a token.
- The fan-out in google-calendar-sa minted a token per impersonated email
  but wrote all of them into the same `MESH_REQUEST_CONTEXT.authorization`
  inside a `Promise.all`. Every branch read whichever token landed last,
  so `list_events` across two users returned one user's calendar
  duplicated. Each branch now builds its tool against a cloned env.

Also drops a `console.log` that printed the service account and
impersonated emails on every refresh, and teaches `parseServiceAccountKey`
to recognize an OAuth `client_secret.json` — the most common paste
mistake — instead of failing on a missing `type` field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…unt auth

Server-to-server access to GA4 with no OAuth login and no refresh token to
keep alive. The app reuses the eight google-analytics tools verbatim via
subpath exports and swaps the bearer for a service account token.

GA4 grants access to the service account identity directly — you add its
email as a property user — so unlike google-calendar-sa there is no
domain-wide delegation, no impersonation list, and no fan-out.

Two credential modes, resolved in one place: a key pasted into the
connection's SERVICE_ACCOUNT_JSON wins, otherwise the managed key from the
GOOGLE_SERVICE_ACCOUNT_JSON site secret is used. The first keeps quota and
audit trail in the customer's Google Cloud project; the second lets them
skip key handling entirely and just grant a Viewer role.

A `check-service-account-access` tool makes that setup self-service: it
reports which credential is active, which email to grant, which properties
are currently readable, and the exact next step when something is missing.
It returns failures as data rather than throwing, so a misconfigured
install always gets an actionable answer instead of a generic error.

Unlike the MCPs it builds on, this one wraps its transport in `withAuth`
and is not added to auth-exemptions.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JonasJesus42
JonasJesus42 enabled auto-merge (squash) August 24, 2026 18:00
@JonasJesus42
JonasJesus42 disabled auto-merge August 24, 2026 18:01
@JonasJesus42
JonasJesus42 merged commit 03ec84f into main Aug 24, 2026
2 checks passed
tlgimenes added a commit that referenced this pull request Aug 27, 2026
Unrelated to this branch's github change — `check:registry` has been failing on
`main` since #550: `google-analytics-sa/app.json` carries a trailing space in
`short_description`, but the `registry.json` committed alongside it does not, so
the generator's output has never matched the checked-in artifact.

Fixed at the source rather than by regenerating, so the stray space isn't baked
into the published registry: with this, `bun run check:registry` is clean and
`registry.json` needs no change at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tlgimenes added a commit that referenced this pull request Aug 27, 2026
…#551)

* fix(github): allow deployments:read so PR cards can show preview URLs

Studio's PR card resolves `previewUrl` from four sources; for a VTEX FastStore
WebOps repo the preview is published ONLY as a GitHub Deployment, so the only
source that finds it is `GET_PREVIEW_DEPLOYMENT` against the Deployments API.
That call needs `deployments:read`, which `ALLOWED_PERMISSIONS` did not permit —
so `capPermissions` hard-rejected it, Studio's mint ladder shed it, and every
repo-scoped token 403'd on the endpoint:

  Not authorized to read deployments for <owner>/<repo> (403).
  The token may lack deployments:read.

Across production, 0 of 207 repo-scoped connections carry `deployments` (92
carry `checks`, minted as recently as today) — the allowlist, not any single
org's installation, is what has been gating it.

- Add `deployments` to ALLOWED_PERMISSIONS.
- Add READ_ONLY_PERMISSIONS (metadata, checks, deployments): capped to `read`
  whatever is asked. `checks:write` would let a token post a green check run,
  and Studio gates PR merges on check status; `deployments:write` would let it
  write the `environment_url` the PR panel renders as a preview link.
- Generalize the refresh path's checks-only widening into an ordered ladder
  (OPTIONAL_READ_UPGRADES + buildUpgradeLadder). GitHub 422s the WHOLE mint when
  any requested permission exceeds the installation's grant, so asking for both
  optionals at once would have 422'd every grant that already had `checks` — and
  the old code, which only retried when it had just added `checks`, would have
  gone straight to handleMintFailure and REVOKED those still-valid grants. The
  ladder sheds one optional at a time and ends at the grant's stored set.
- Derive the tool's permissions description from the allowlist; the hand-written
  list had already gone stale when `checks` was added.
- README: document the repository permissions the GitHub App must declare and
  that each installation must accept the request before it takes effect.

Existing grants self-heal on their next ~1h `/repo-grant/token` refresh once
their installation approves the permission — no re-import, no re-install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(google-analytics-sa): drop trailing space in short_description

Unrelated to this branch's github change — `check:registry` has been failing on
`main` since #550: `google-analytics-sa/app.json` carries a trailing space in
`short_description`, but the `registry.json` committed alongside it does not, so
the generator's output has never matched the checked-in artifact.

Fixed at the source rather than by regenerating, so the stray space isn't baked
into the published registry: with this, `bun run check:registry` is clean and
`registry.json` needs no change at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant