feat(google-analytics-sa): GA4 MCP with service account auth - #550
Merged
Conversation
…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
enabled auto-merge (squash)
August 24, 2026 18:00
JonasJesus42
disabled auto-merge
August 24, 2026 18:01
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 unlikegoogle-calendar-sathere is no domain-wide delegation, no impersonation list and no fan-out; credentials resolve in one place, preferring a key pasted into the connection'sSERVICE_ACCOUNT_JSONand falling back to a managed key from theGOOGLE_SERVICE_ACCOUNT_JSONsite secret, so a customer can either keep quota in their own GCP project or just grant a Viewer role and skip key handling. Acheck-service-account-accesstool 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-sainto@decocms/mcps-shared/google-service-account, which fixes three bugs on the way:subjectbecomes optional (thesubclaim must be absent for GA4), the token cache key now includesclient_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 sharedMESH_REQUEST_CONTEXT.authorizationinside aPromise.all, which madelist_eventsacross two users return one user's calendar duplicated. The new app wraps its transport inwithAuthand is not added toauth-exemptions.json; 14 tests cover the JWT claims, cache isolation, error propagation and credential precedence, and all fourcheck-service-account-accessfailure paths were exercised against a running server.Deploying requires setting the
GOOGLE_SERVICE_ACCOUNT_JSONsecret on thegoogle-analytics-sasite — without it only the bring-your-own-key mode works. The happy path (ok: truewith 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-accountand fixes cache and fan-out bugs ingoogle-calendar-sa.google-analyticstools via new subpath exports; tools read the bearer fromMESH_REQUEST_CONTEXTand run with a cloned env to avoid shared mutation.check-service-account-accesstool reports active credential source, service account email to grant, readable properties, and next steps; returns failures as data.@decocms/mcps-shared/google-service-account: makessubjectoptional and last; cache key now includesclient_email; uses WebCrypto only; clearer key parsing errors (detects OAuth client files).google-calendar-sanow clones env per impersonated user and stops writing tokens into a sharedauthorizationduring fan-out.google-analyticsadds exports (./tools,./constants,./client,./schemas,./types) and marksGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECREToptional for service-account builds.withAuth; app is added todeploy.jsonandregistry.json. Tests cover JWT claims, cache isolation, env cloning, error propagation, and credential precedence.Bolded section: Rollout and migration
GOOGLE_SERVICE_ACCOUNT_JSONsite secret on thegoogle-analytics-sasite to enable managed credentials, or paste a key into the connection’sSERVICE_ACCOUNT_JSON.check-service-account-accessto confirm.getServiceAccountAccessTokento the new signature:(serviceAccountJson, scopes, subject?).Written for commit 42cc9bd. Summary will update on new commits.