{
  "markdown": "# Samarth GTM MCP Server\n\n[![CI](https://github.com/samarthanalytics-sj/samarth-analytics-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/samarthanalytics-sj/samarth-analytics-mcp/actions/workflows/ci.yml)\n\nA production-ready [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the **Google Tag Manager API v2**, built for Samarth Analytics.\n\nUse it through **Samarth Desktop** - the local Electron app with a chat UI that embeds this server ([Quick Start](#quick-start)). Full, guarded access to GTM: read workspace contents, create/update tags/triggers/variables, audit implementations, publish versions, and more.\n\n> **New: browser portal with live QC audit.** A white-label, browser-based customer experience lives in [`apps/portal/`](./apps/portal/README.md). Customers sign in with Google OAuth, pick a GTM account/container/workspace, and run a live, read-only QC audit. Publishes still require Samarth approval. See the portal README for OAuth setup; run with `npm run portal:dev`.\n\n---\n\n## Table of Contents\n\n1. [Features](#features)\n2. [Quick Start](#quick-start)\n3. [Google Cloud OAuth Setup](#google-cloud-oauth-setup)\n4. [Service Account Limitations](#service-account-limitations)\n5. [Environment Variables Reference](#environment-variables-reference)\n6. [Guardrails](#guardrails)\n7. [Available Tools](#available-tools)\n8. [Cloud Deployment](#cloud-deployment)\n9. [Security Notes](#security-notes)\n10. [Development](#development)\n11. [Releases](#releases)\n12. [Troubleshooting](#troubleshooting)\n\n---\n\n## Features\n\n- **Full GTM API v2 surface** — accounts, containers, workspaces, tags, triggers, variables, folders, built-in variables, versions, sync, publish, preview\n- **Server-side & advanced GTM coverage** — environments, user permissions, destinations, clients, transformations, zones, custom templates, gtag config, plus container snippet/lookup/combine/move-tag-id and workspace change-diff status\n- **GA4 coverage** — GA4 Admin tools (`ga4_*`) plus GA4 Data API reporting (`ga4_run_report`, `ga4_run_realtime_report`) for intent-vs-reality reconciliation. Reads and reporting need only `analytics.readonly`; the GA4 Admin **write** tools are off by default behind `GA4_MCP_ENABLE_WRITES` / `GA4_MCP_ENABLE_DELETES` and additionally need `analytics.edit`\n- **Automatic pagination** — every paginated list tool transparently follows `nextPageToken` to return all results, with optional `maxPages`/`pageToken` bounds\n- **Retry with exponential backoff + jitter** — transient Google API failures (HTTP 408/429/5xx, network errors) on read requests are retried automatically; mutations are never auto-retried (tunable via `GTM_MCP_RETRY_*`)\n- **Two transport modes**: stdio (local, for Claude Desktop/Cursor) and Streamable HTTP (cloud/team)\n- **Guardrails by default**: read-only unless explicitly enabled; publish and delete gated separately\n- **Dry-run mode**: simulate all writes without touching the API\n- **`confirm=true` required** on all write/delete/publish operations\n- **Audit tool**: inspects workspace for common GA4/GTM implementation issues\n- **Export tool**: full workspace dump as structured JSON\n- **Zod schema validation** on all inputs\n- **Detailed Google API error messages** surfaced to the MCP client\n\n---\n\n## Quick Start\n\nThe way to run Samarth is the **desktop app** - a local Electron app with a chat\nUI that embeds the MCP server in-process. Multi-account Google sign-in,\nper-account LLM keys (OpenAI / Anthropic / Gemini), secrets in the OS keychain.\nNothing to configure by hand: the Google OAuth client and your LLM key are\nentered in the app on first run.\n\n**You need:** [Node.js](https://nodejs.org) 18 or newer, [Git](https://git-scm.com),\nand a free Google \"Desktop app\" OAuth client - two values you create once in\nyour Google Cloud project ([exact click-by-click steps](#google-cloud-oauth-setup)).\n\n```bash\ngit clone https://github.com/samarthanalytics-sj/samarth-analytics-mcp.git\ncd samarth-analytics-mcp/apps/desktop\nnpm install    # downloads the Electron binary (~100 MB first time)\nnpm run dev\n```\n\nThe window opens; paste your OAuth client ID + secret and an LLM API key when\nasked, sign in to Google, and start chatting with your GTM / GA4 setup.\n\n**Prefer a config file over typing in the app?** Put the OAuth client in a\n`.env` file instead - the app reads it on launch (values typed in the app and\nreal shell variables always take precedence):\n\n```ini\nGOOGLE_DESKTOP_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_DESKTOP_CLIENT_SECRET=your-client-secret\n```\n\nSave it as `apps/desktop/.env` or at the repo root (already gitignored - never\ncommit it). A repo-root `.env` using the server's `GOOGLE_OAUTH_CLIENT_ID` /\n`GOOGLE_OAUTH_CLIENT_SECRET` names works too, so one file can serve both the\napp and the MCP server. A packaged install reads `.env` from its data\ndirectory instead (`%APPDATA%` on Windows).\n\nYour **everyday launch** afterwards is just:\n\n```bash\ncd samarth-analytics-mcp/apps/desktop && npm run dev\n```\n\nFull guide - Windows/macOS specifics, first-run setup, building a real\n`.exe`/`.dmg` installer, and the `Error: Electron uninstall` fix:\n[apps/desktop/INSTALL.md](apps/desktop/INSTALL.md).\n\n---\n\n## Google Cloud OAuth Setup\n\n### Step 1: Enable the GTM API\n\n1. Go to [Google Cloud Console](https://console.cloud.google.com)\n2. Select or create a project\n3. Navigate to **APIs & Services → Library**\n4. Search for **\"Tag Manager API\"** and click **Enable**\n5. Search for **\"Google Analytics Admin API\"** and click **Enable** (required for the read-only `ga4_*` Admin tools)\n6. Search for **\"Google Analytics Data API\"** and click **Enable** (required for `ga4_run_report` / `ga4_run_realtime_report`)\n\n### Step 2: Create OAuth 2.0 Credentials\n\n1. Go to **APIs & Services → Credentials**\n2. Click **Create Credentials → OAuth 2.0 Client ID**\n3. Choose application type:\n   - **Desktop app** — simplest for local stdio use (no redirect URI needed)\n   - **Web application** — for the HTTP server (add your redirect URI)\n4. Download the JSON or copy the **Client ID** and **Client Secret**\n5. Add to `.env`:\n   ```\n   GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\n   GOOGLE_CLIENT_SECRET=your-client-secret\n   GOOGLE_REDIRECT_URI=http://localhost:3001/oauth/callback\n   ```\n\n### Step 3: Configure OAuth Consent Screen\n\n1. Go to **APIs & Services → OAuth consent screen**\n2. Choose **External** (or Internal if you have Google Workspace)\n3. Fill in App name, support email\n4. Add scopes:\n   - `https://www.googleapis.com/auth/tagmanager.readonly`\n   - `https://www.googleapis.com/auth/tagmanager.edit.containers`\n   - `https://www.googleapis.com/auth/tagmanager.edit.containerversions`\n   - `https://www.googleapis.com/auth/tagmanager.manage.accounts`\n   - `https://www.googleapis.com/auth/tagmanager.manage.users`\n   - `https://www.googleapis.com/auth/tagmanager.publish`\n   - `https://www.googleapis.com/auth/analytics.readonly` (read-only GA4 Admin **and** Data API tools)\n   - `https://www.googleapis.com/auth/analytics.edit` (GA4 Admin write tools — gated by `GA4_MCP_ENABLE_WRITES`)\n   - `https://www.googleapis.com/auth/analytics.manage.users` (GA4 access-binding write tools)\n5. Add your Google account as a **test user** (while the app is in \"testing\" mode)\n\n> **Note**: For personal/agency use, keeping the app in \"Testing\" mode is fine. You will need to re-authorize every 7 days unless you publish the app or get it verified.\n\n### Step 4: Run OAuth Setup\n\n```bash\nnpm run auth:google\n```\n\nOr, if you prefer the older paste-the-code helper:\n\n```bash\nnpm run oauth:setup\n```\n\n---\n\n## Service Account Limitations\n\n> **Short version**: Service accounts do NOT work with GTM by default. Use OAuth 2.0.\n\nThe Google Tag Manager API is a **user-data API** — it manages resources owned by individual Google accounts. Service accounts are not Google users and are not automatically granted access to GTM containers.\n\n### Option A: Add the service account as a GTM user (simplest)\n\nIf you still want to use a service account:\n\n1. Get the service account email (e.g., `my-sa@project.iam.gserviceaccount.com`)\n2. In GTM, go to **Admin → User Management** at the account or container level\n3. Add the service account email with the appropriate role (Read, Edit, Approve, Publish)\n4. Set `GOOGLE_SERVICE_ACCOUNT_KEY_FILE=/path/to/key.json` in `.env`\n\n**Caveats**: This only works if the GTM container is associated with a Google account, not a Google Workspace that restricts external sharing.\n\n### Option B: Domain-Wide Delegation (Google Workspace only)\n\nFor Google Workspace organizations:\n\n1. Create a service account with a JSON key\n2. Enable **Domain-Wide Delegation** on the service account in Google Cloud Console\n3. In **Google Workspace Admin Console → Security → API Controls → Domain-wide Delegation**:\n   - Add the service account client ID\n   - Add scopes: `https://www.googleapis.com/auth/tagmanager.edit.containers` (and others as needed)\n4. In `.env`, set:\n   ```\n   GOOGLE_SERVICE_ACCOUNT_KEY_FILE=/path/to/key.json\n   ```\n5. The server will impersonate the user automatically if you set a subject in `buildGoogleAuth()`\n\n**Caveats**: Requires a paid Google Workspace account. Only available for your own domain.\n\n---\n\n## Environment Variables Reference\n\n| Variable | Default | Description |\n|---|---|---|\n| `GOOGLE_OAUTH_CLIENT_ID` | — | OAuth client ID (preferred). Falls back to `GOOGLE_CLIENT_ID`. |\n| `GOOGLE_OAUTH_CLIENT_SECRET` | — | OAuth client secret (preferred). Falls back to `GOOGLE_CLIENT_SECRET`. |\n| `GOOGLE_OAUTH_REDIRECT_URI` | `http://localhost:3001/oauth/callback` | OAuth redirect URI. Falls back to `GOOGLE_REDIRECT_URI`. |\n| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `GOOGLE_REDIRECT_URI` | — | Legacy names, still supported. |\n| `SAMARTH_GOOGLE_OAUTH_CLIENT_ID` | — | **Hosted-only.** Samarth-owned public OAuth client. Takes precedence over the self-hosted vars when set. |\n| `SAMARTH_GOOGLE_OAUTH_CLIENT_SECRET` | — | **Hosted-only.** Inject from your platform secret manager. Never commit. |\n| `GOOGLE_ACCESS_TOKEN` | — | Current OAuth access token. Env vars take precedence over the token file. |\n| `GOOGLE_REFRESH_TOKEN` | — | OAuth refresh token (long-lived). Env vars take precedence over the token file. |\n| `GTM_MCP_TOKEN_FILE` | `./.gtm-mcp-tokens.json` | Path to the local OAuth token file written by `npm run auth:google` (gitignored). |\n| `GOOGLE_SERVICE_ACCOUNT_KEY_FILE` | — | Path to service account JSON key (see limitations above) |\n| `GTM_MCP_TRANSPORT` | `stdio` | Transport: `stdio` or `http` |\n| `GTM_MCP_HTTP_PORT` | `3001` | HTTP server port (http transport only; falls back to `PORT`) |\n| `GTM_MCP_HTTP_AUTH_TOKEN` | — | Bearer token gating `/mcp` (http transport). With neither this nor `STYTCH_PROJECT_ID` set, the HTTP transport **refuses to start**. |\n| `GTM_MCP_HTTP_ALLOW_UNAUTHENTICATED` | `false` | Local-dev opt-in to start without auth. Binds loopback only unless `GTM_MCP_HTTP_HOST` overrides. |\n| `GTM_MCP_HTTP_HOST` | — | Bind host. Defaults to loopback when unauthenticated, all interfaces when authenticated. |\n| `STYTCH_PROJECT_ID` | — | Setting this switches the HTTP transport to **multi-user mode**: each `/mcp` request carries a Stytch JWT resolved to that user's own Google identity. Unset = single-identity mode. Pin `STYTCH_JWT_ISSUER` / `STYTCH_JWT_AUDIENCE` in production (see `.env.example`). |\n| `STYTCH_SECRET` | — | Stytch project secret (server-only). Required when `STYTCH_PROJECT_ID` is set — the server exits without it. |\n| `STYTCH_PUBLIC_TOKEN` | — | Publishable token powering the `/oauth/authorize` page. Not a secret. |\n| `GTM_MCP_PUBLIC_URL` | `http://localhost:<port>` | This server's public origin, advertised in the OAuth Protected Resource Metadata document. |\n| `GTM_MCP_ENABLE_WRITES` | `false` | Allow create/update operations |\n| `GTM_MCP_ENABLE_PUBLISH` | `false` | Allow publish operations |\n| `GTM_MCP_ENABLE_DELETES` | `false` | Allow delete operations |\n| `DRY_RUN` | `false` | Simulate all writes without calling the API |\n| `GTM_MCP_RETRY_MAX` | `3` | Retry attempts for transient read failures (408/429/5xx, network). `0` disables retries. Mutations are never auto-retried. |\n| `GTM_MCP_RETRY_MAX_DELAY_MS` | `30000` | Cap on a single backoff sleep (exponential backoff with jitter) |\n| `GTM_MCP_RETRY_TOTAL_TIMEOUT_MS` | `60000` | Cap on total wall time from first request to last retry |\n\n---\n\n## Guardrails\n\nThe server enforces three independent guardrails in addition to the `confirm=true` requirement:\n\n| Guardrail | Env Variable | What it gates |\n|---|---|---|\n| **Write guard** | `GTM_MCP_ENABLE_WRITES=true` | All `create` and `update` operations |\n| **Delete guard** | `GTM_MCP_ENABLE_DELETES=true` | All `delete` operations |\n| **Publish guard** | `GTM_MCP_ENABLE_PUBLISH=true` | All version publish operations |\n| **Dry run** | `DRY_RUN=true` | Simulate without API calls (overrides all) |\n\n**`confirm=true` is always required** on write/delete/publish tools regardless of env settings. This prevents accidental modifications even when guardrails are enabled.\n\n### Recommended Configurations\n\n**Read-only exploration** (default — safe for sharing with team):\n```env\nGTM_MCP_ENABLE_WRITES=false\nGTM_MCP_ENABLE_PUBLISH=false\nGTM_MCP_ENABLE_DELETES=false\n```\n\n**Development workspace edits** (no publishing):\n```env\nGTM_MCP_ENABLE_WRITES=true\nGTM_MCP_ENABLE_PUBLISH=false\nGTM_MCP_ENABLE_DELETES=false\n```\n\n**Full access** (use with care):\n```env\nGTM_MCP_ENABLE_WRITES=true\nGTM_MCP_ENABLE_PUBLISH=true\nGTM_MCP_ENABLE_DELETES=true\n```\n\n---\n\n## Available Tools\n\n### Accounts\n| Tool | Description |\n|---|---|\n| `accounts_list` | List all accessible GTM accounts |\n| `accounts_get` | Get a specific account |\n\n### Containers\n| Tool | Description |\n|---|---|\n| `containers_list` | List containers in an account (auto-paginated) |\n| `containers_get` | Get a specific container |\n| `containers_create` | ✏️ Create a new container |\n| `containers_snippet` | Get the GTM installation snippet for a container |\n| `containers_lookup` | Look up a container by linked destination/tag ID (e.g. `G-XXXX`) |\n| `containers_combine` | ✏️ Combine (merge) another container into this one |\n| `containers_move_tag_id` | ✏️ Move a Tag ID out into a new container |\n\n### Destinations\n| Tool | Description |\n|---|---|\n| `destinations_list` | List linked destinations (Google tags / GA4) for a container |\n| `destinations_get` | Get a specific destination |\n| `destinations_link` | ✏️ Link a destination to a container |\n\n### Workspaces\n| Tool | Description |\n|---|---|\n| `workspaces_list` | List workspaces in a container (auto-paginated) |\n| `workspaces_get` | Get a specific workspace |\n| `workspaces_create` | ✏️ Create a new workspace |\n| `workspace_get_status` | Review the change diff (changed entities + merge conflicts) before versioning |\n| `workspace_sync` | ✏️ Sync workspace to latest container version |\n| `workspace_resolve_conflict` | ✏️ Resolve a merge conflict |\n| `workspace_quick_preview` | Generate a preview link (read-safe) |\n| `workspace_create_version_and_publish` | 🚀 Create version + publish in one step |\n\n### Tags\n| Tool | Description |\n|---|---|\n| `tags_list` | List all tags in a workspace |\n| `tags_get` | Get a specific tag |\n| `tags_create` | ✏️ Create a tag |\n| `tags_update` | ✏️ Update a tag |\n| `tags_delete` | 🗑️ Delete a tag |\n\n### Triggers\n| Tool | Description |\n|---|---|\n| `triggers_list` | List all triggers |\n| `triggers_get` | Get a specific trigger |\n| `triggers_create` | ✏️ Create a trigger |\n| `triggers_update` | ✏️ Update a trigger |\n| `triggers_delete` | 🗑️ Delete a trigger |\n\n### Variables\n| Tool | Description |\n|---|---|\n| `variables_list` | List all user-defined variables |\n| `variables_get` | Get a specific variable |\n| `variables_create` | ✏️ Create a variable |\n| `variables_update` | ✏️ Update a variable |\n| `variables_delete` | 🗑️ Delete a variable |\n\n### Folders\n| Tool | Description |\n|---|---|\n| `folders_list` | List all folders |\n| `folders_get` | Get a specific folder |\n| `folders_entities` | List entities in a folder (auto-paginated) |\n| `folders_create` | ✏️ Create a folder |\n| `folders_update` | ✏️ Update a folder |\n| `folders_delete` | 🗑️ Delete a folder |\n| `folders_move_entities` | ✏️ Move entities into a folder |\n\n### Built-In Variables\n| Tool | Description |\n|---|---|\n| `built_in_variables_list` | List enabled built-in variables |\n| `built_in_variables_enable` | ✏️ Enable built-in variables |\n| `built_in_variables_disable` | 🗑️ Disable built-in variables |\n| `built_in_variables_revert` | ✏️ Revert a built-in variable to base version |\n\n### Versions\n| Tool | Description |\n|---|---|\n| `versions_list` | List version headers |\n| `versions_get` | Get a version (pass \"live\" for current live version) |\n| `versions_create` | ✏️ Create a checkpoint version from workspace |\n| `versions_set_latest` | ✏️ Set a version as latest |\n| `versions_publish` | 🚀 Publish a specific version |\n| `versions_undelete` | ✏️ Undelete a version |\n| `versions_delete` | 🗑️ Delete a version |\n\n### Environments\n| Tool | Description |\n|---|---|\n| `environments_list` | List environments in a container (auto-paginated) |\n| `environments_get` | Get a specific environment |\n| `environments_create` | ✏️ Create an environment |\n| `environments_update` | ✏️ Update an environment |\n| `environments_reauthorize` | 🚀 Re-generate the environment authorization token (high-impact) |\n| `environments_delete` | 🗑️ Delete an environment |\n\n### User Permissions (account-level)\n| Tool | Description |\n|---|---|\n| `user_permissions_list` | List user permissions for an account (auto-paginated) |\n| `user_permissions_get` | Get a specific user permission |\n| `user_permissions_create` | ✏️ Grant a user account/container access |\n| `user_permissions_update` | ✏️ Update a user's access levels |\n| `user_permissions_delete` | 🗑️ Revoke a user's access |\n\n### Server-Side & Advanced Container Resources\n\nThese are workspace-scoped resources from GTM API v2. Create/update accept the full\nresource as a JSON string (`bodyJson`) since their bodies are deeply nested. Each\nsupports `*_list` (auto-paginated), `*_get`, `*_create` ✏️, `*_update` ✏️, `*_delete` 🗑️,\nand (except `gtag_config`) `*_revert` ✏️.\n\n| Resource | Tools | Notes |\n|---|---|---|\n| Clients | `clients_*` | Server container request clients |\n| Transformations | `transformations_*` | Server container event transformations |\n| Zones | `zones_*` | Zone delegation |\n| Templates | `templates_*` | Custom / gallery-installed templates |\n| Gtag Config | `gtag_config_*` | Google tag (gtag) configuration — no `revert` |\n\n### Analytics & Export\n| Tool | Description |\n|---|---|\n| `audit_container` | Inspect workspace for analytics issues |\n| `export_container` | Export workspace as structured JSON |\n\n### GA4 Admin (read-only)\n\nRead-only wrappers over the **Google Analytics Admin API** (v1beta, with a single\nv1alpha call for enhanced measurement). These never write, update, or delete GA4\nresources and require no `confirm` flag. They power the senior audit framework's\nGA4_ADMIN checks (custom dimensions/metrics, data streams & measurement IDs, data\nretention, enhanced measurement, key events, Google Ads links).\n\nRequires the `https://www.googleapis.com/auth/analytics.readonly` scope and the\n**Google Analytics Admin API** enabled in your Google Cloud project. A 403 mentioning\nscope means you should re-run `npm run auth:google`.\n\n| Tool | Description |\n|---|---|\n| `ga4_account_summaries_list` | List GA4 accounts + their property summaries (best discovery entry point) |\n| `ga4_properties_list` | List properties under a parent account (display name, time zone, currency, service level) |\n| `ga4_property_get` | Get a single property by ID |\n| `ga4_data_streams_list` | List data streams (web/Android/iOS) incl. web **measurement IDs** |\n| `ga4_enhanced_measurement_get` | Get enhanced measurement settings for a web data stream (v1alpha) |\n| `ga4_custom_dimensions_list` | List custom dimensions (parameter, scope) |\n| `ga4_custom_metrics_list` | List custom metrics (parameter, unit, scope) |\n| `ga4_data_retention_get` | Get event data-retention settings |\n| `ga4_key_events_list` | List key events (formerly \"conversion events\" — current Admin naming) |\n| `ga4_google_ads_links_list` | List Google Ads links (customer ID, auto-tagging/ads-personalization flags) |\n\nAccepts either a bare numeric ID (`123456789`) or the fully-qualified form\n(`properties/123456789`, `accounts/123456`) wherever a property/account is required.\n\n**Documented limitations** (not exposed by the GA4 Admin API v1beta, so intentionally\n**not** implemented rather than faked):\n\n- Internal-traffic / unwanted-referral **data filters** — no public `dataFilters` collection; configured per data stream.\n- **Referral exclusions** — no dedicated Admin API resource.\n- **Channel groups** and **audiences** — exist only on the v1alpha surface and are out of scope for this read-only v1beta set.\n\n### GA4 Data API (read-only reporting)\n\nRead-only wrappers over the **Google Analytics Data API** (v1beta). They never\nwrite and require no `confirm` flag. Use them to reconcile *intent vs. reality* —\ncomparing the events a container is configured to send against the events GA4\nactually reports (zero reported activity for a configured event is a red flag).\n\nThese use the **same** `https://www.googleapis.com/auth/analytics.readonly` scope\nas the GA4 Admin tools, so no extra consent is needed. Enable the **Google\nAnalytics Data API** in your Google Cloud project.\n\n| Tool | Description |\n|---|---|\n| `ga4_run_report` | Run a report over a date range (dimensions + metrics, e.g. `eventCount` by `eventName`); supports `limit`, `offset`, and ordering |\n| `ga4_run_realtime_report` | Run a Realtime report (events in roughly the last 30 minutes) for live QA |\n\n**Documented gaps** (intentionally not exposed rather than faked): pivot reports,\ncohorts, and funnels.\n\n### Pagination\n\nAll list tools backed by paginated GTM endpoints (`accounts_*`-scoped containers,\nworkspaces, tags, triggers, variables, folders, folder entities, environments,\nuser permissions, clients, transformations, zones, templates, gtag configs)\n**auto-follow pagination and return all results by default**. Optional arguments:\n\n- `maxPages` — cap the number of API pages fetched (default 50). If more pages remain,\n  the response includes `\"truncated\": true` and a `nextPageToken`.\n- `pageToken` — resume from a previous truncated result.\n\nNon-truncated responses keep the original `{ <key>: [...], count }` shape unchanged.\n\nTwo tools differ, because one list key does not describe what they return:\n\n- `folders_entities` returns three parallel collections (`tag`, `trigger`, `variable`, always\n  present, empty when the folder has none) plus a `counts` object, and adds `truncated` /\n  `nextPageToken` only when the page ceiling was hit.\n- `export_container` pages five collections independently, so it takes `maxPages` (applied per\n  collection) but no `pageToken`. A short export is marked `incomplete: true` with\n  `truncatedCollections`, per-collection `nextPageTokens` and a `warning` — in every format,\n  including the default `summary`.\n\nLegend: ✏️ requires `GTM_MCP_ENABLE_WRITES=true` | 🗑️ requires `GTM_MCP_ENABLE_DELETES=true` | 🚀 requires `GTM_MCP_ENABLE_PUBLISH=true`\n\nAll ✏️ 🗑️ 🚀 tools also require `confirm: true` in the tool arguments.\n\n---\n\n## Cloud Deployment\n\n### Transport\n\nFor cloud deployments, use `GTM_MCP_TRANSPORT=http`. The server exposes:\n- `POST /mcp` — Streamable HTTP MCP endpoint\n- `GET /mcp` — SSE stream for existing sessions\n- `DELETE /mcp` — Session termination\n- `GET /health` — Health check\n\nThere is no `/oauth/callback` route on this server. `npm run auth:google` runs its own short-lived listener on `127.0.0.1:3001` for the redirect; an unauthenticated callback on the hosted transport could overwrite the server's stored Google credentials, so it was removed.\n\n### Connecting Remote Clients\n\nClients that support Streamable HTTP can connect directly to the `/mcp` endpoint. For clients that only support stdio (like Claude Desktop), use [mcp-remote](https://www.npmjs.com/package/mcp-remote) as a proxy:\n\n```json\n{\n  \"mcpServers\": {\n    \"samarth-gtm-remote\": {\n      \"command\": \"npx\",\n      \"args\": [\"mcp-remote@next\", \"https://your-server.com/mcp\"]\n    }\n  }\n}\n```\n\n### Vercel\n\n> **Limitation**: Vercel Serverless Functions have a 10-second timeout (hobby) / 60-second (pro). Stateful SSE sessions require persistent connections which Vercel does not support well. Use Vercel only for **stateless** MCP interactions. Recommended alternative: Vercel + external session store (Redis/Upstash), or use Render/Fly.io instead.\n\nFor Vercel, export the Express app as a serverless handler:\n```ts\n// api/mcp.ts\nexport default app; // where app is the Express instance\n```\n\nSet env vars in Vercel Dashboard → Settings → Environment Variables.\n\n### Render\n\n1. Create a new **Web Service** in [Render](https://render.com)\n2. Connect your GitHub repo\n3. Build command: `npm install && npm run build`\n4. Start command: `GTM_MCP_TRANSPORT=http node dist/index.js`\n5. Add environment variables in Render Dashboard\n6. **Important**: Set `RENDER=true` env var and ensure your health check hits `/health`\n\nRender supports persistent long-lived connections — recommended for SSE/streaming.\n\n### Fly.io\n\n```bash\nfly launch\nfly secrets set GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx\nfly secrets set GTM_MCP_TRANSPORT=http GTM_MCP_HTTP_PORT=3001\nfly deploy\n```\n\nFly.io has no request timeout limitations and supports persistent WebSocket/SSE connections. Recommended for production.\n\n### Docker\n\n```dockerfile\nFROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY dist/ ./dist/\nENV GTM_MCP_TRANSPORT=http\nENV GTM_MCP_HTTP_PORT=3001\nEXPOSE 3001\nCMD [\"node\", \"dist/index.js\"]\n```\n\n---\n\n## Security Notes\n\n1. **Never commit `.env`** — it contains OAuth tokens. `.env` is already in `.gitignore`.\n\n2. **Rotate tokens regularly** — OAuth refresh tokens are long-lived but can be revoked. Revoke at [myaccount.google.com/permissions](https://myaccount.google.com/permissions).\n\n3. **Minimum scopes** — If you only need read access, revoke write scopes by removing them from the OAuth consent screen and re-authorizing. The server reads fine with `tagmanager.readonly` only.\n\n4. **Cloud deployment**: Store secrets in your platform's secret manager (Render Secrets, Fly.io Secrets, Vercel Env Vars), never in code or Docker images.\n\n5. **HTTP transport**: `/mcp` has two built-in auth modes — `GTM_MCP_HTTP_AUTH_TOKEN` (one shared bearer token, identifies the deployment) and `STYTCH_PROJECT_ID` (per-user OAuth, each request resolved to that user's own Google grant). With neither set the transport refuses to start; `GTM_MCP_HTTP_ALLOW_UNAUTHENTICATED=true` overrides that for local development and binds loopback only.\n\n6. **Publish guard**: Keep `GTM_MCP_ENABLE_PUBLISH=false` unless you explicitly intend to publish from an AI client. Publishing incorrect tags to production is the highest-risk operation.\n\n7. **Audit logs**: The server logs all session events to stderr. Pipe to a logging service in production.\n\n---\n\n## Development\n\n```bash\n# Install dependencies\nnpm install\n\n# TypeScript type check (no emit)\nnpm run typecheck\n\n# Build\nnpm run build\n\n# Watch mode\nnpm run build:watch\n\n# Run dev server (stdio, with hot-reload)\nnpm run dev\n\n# Run HTTP dev server\nnpm run dev:http\n\n# Tests (run `npm run build` first — some suites test the compiled dist)\nnpm test\n\n# Smoke test: server boots and answers tools/list\nnpm run smoke -- --mcp dist/index.js\n\n# Full-surface smoke test: invokes ALL registered tools with a sanitized,\n# credential-free env — every handler must respond cleanly (no crash/hang)\nnpm run smoke:all\n\n# MCP Inspector (interactive tool debugging)\nnpm run inspector\n```\n\n### Project Structure\n\n```\nsamarth-gtm-mcp/\n├── src/\n│   ├── index.ts              # Entry point — stdio/HTTP transport setup\n│   ├── server.ts             # MCP server factory + tool registration\n│   ├── auth/\n│   │   └── googleAuth.ts     # OAuth2 / service account auth\n│   ├── tools/\n│   │   ├── index.ts          # Tool registration aggregator\n│   │   ├── accounts.ts       # accounts/list, accounts/get\n│   │   ├── containers.ts     # containers/list/get/create\n│   │   ├── workspaces.ts     # workspaces + sync/resolve_conflict\n│   │   ├── tags.ts           # tags CRUD\n│   │   ├── triggers.ts       # triggers CRUD\n│   │   ├── variables.ts      # variables CRUD\n│   │   ├── folders.ts        # folders CRUD + move_entities\n│   │   ├── builtInVariables.ts # enable/disable/revert built-ins\n│   │   ├── versions.ts       # versions list/get/create/publish/delete\n│   │   ├── publish.ts        # quick_preview, versions_publish, create+publish\n│   │   ├── audit.ts          # audit_container analytics checks\n│   │   ├── export.ts         # export_container JSON dump\n│   │   ├── environments.ts   # environments CRUD + reauthorize\n│   │   ├── userPermissions.ts # account-level user permissions\n│   │   ├── serverSide.ts     # clients, transformations, zones, templates, gtag config\n│   │   ├── ga4Admin.ts       # read-only GA4 Admin tools (ga4_*)\n│   │   └── ga4Data.ts        # read-only GA4 Data API reporting\n│   ├── utils/\n│   │   ├── guardrails.ts     # Guardrail enforcement, error formatting\n│   │   ├── gtmClient.ts      # googleapis GTM v2 client factory\n│   │   ├── ga4Client.ts      # GA4 Admin/Data client factories\n│   │   ├── apiRetry.ts       # retry/backoff config (429/5xx, reads only)\n│   │   ├── pagination.ts     # transparent nextPageToken following\n│   │   ├── schemas.ts        # shared Zod input schemas\n│   │   └── toolResponse.ts   # standard tool result shaping\n│   ├── types/\n│   │   ├── gtm.ts            # GTM API type definitions\n│   │   └── index.ts\n│   ├── scripts/\n│   │   ├── auth-google.ts    # Browser-based OAuth onboarding (`npm run auth:google`)\n│   │   └── oauth-setup.ts    # Interactive OAuth token helper (legacy paste-the-code flow)\n│   └── __tests__/\n│       ├── guardrails.node.test.mjs  # guardrails + buildPath\n│       ├── auth.node.test.mjs        # env/auth resolution + token file paths\n│       ├── pagination.node.test.mjs  # paginate/buildListResult\n│       ├── ga4Admin.node.test.mjs    # GA4 tool registration (tests compiled dist)\n│       └── apiRetry.node.test.mjs    # retry/backoff config (tests compiled dist)\n├── scripts/\n│   ├── smoke-test.mjs        # health probe: portal endpoints + MCP tools/list\n│   └── smoke-all-tools.mjs   # invokes all tools with sanitized env\n├── .env.example\n├── .gitignore\n├── package.json\n├── tsconfig.json\n└── README.md\n```\n\n---\n\n## Releases\n\nReleases are fully automated via [semantic-release](https://semantic-release.gitbook.io/) and GitHub Actions. Every push to `main` triggers the [`release.yml`](.github/workflows/release.yml) workflow, which:\n\n1. Installs dependencies, type-checks, builds, and runs tests.\n2. Inspects commits since the last tag using the [Conventional Commits](https://www.conventionalcommits.org/) spec.\n3. Determines the next semantic version (`MAJOR.MINOR.PATCH`).\n4. Updates `CHANGELOG.md` and bumps the `version` in `package.json` / `package-lock.json`.\n5. Commits those files back to `main` with `chore(release): x.y.z [skip ci]` (the `[skip ci]` marker prevents an infinite release loop).\n6. Creates a Git tag (`vX.Y.Z`) and a GitHub Release with auto-generated notes.\n\nThe workflow uses the built-in `GITHUB_TOKEN` and requires no additional secrets. `npm publish` is disabled — this package is distributed as a binary via the GitHub repo and releases, not via the npm registry.\n\n### Conventional Commit Examples\n\nCommit messages drive the version bump:\n\n| Commit prefix       | Effect                  | Example                                                    |\n| ------------------- | ----------------------- | ---------------------------------------------------------- |\n| `fix:`              | Patch release (`x.y.Z`) | `fix: handle empty workspace in audit tool`                |\n| `feat:`             | Minor release (`x.Y.0`) | `feat: add bulk tag import tool`                           |\n| `perf:`             | Patch release           | `perf: cache GTM client between tool calls`                |\n| `docs:` / `chore:` / `refactor:` / `test:` / `style:` / `ci:` / `build:` | No release | `docs: clarify OAuth setup steps` |\n| `BREAKING CHANGE:` footer or `!` after type | Major release (`X.0.0`) | see below                            |\n\n#### Breaking change examples\n\n```\nfeat!: drop support for Node.js 18\n\nBREAKING CHANGE: minimum required Node version is now 20.\n```\n\nor:\n\n```\nrefactor(auth): rename GOOGLE_REFRESH_TOKEN env var\n\nBREAKING CHANGE: GOOGLE_REFRESH_TOKEN is now GTM_GOOGLE_REFRESH_TOKEN.\nUpdate your .env file accordingly.\n```\n\n### Dry run locally\n\nTo preview what the next release would look like without publishing:\n\n```bash\nGITHUB_TOKEN=<a-token-with-no-perms-is-fine-for-dry-run> \\\n  npx semantic-release --dry-run --no-ci\n```\n\n### Manual release skip\n\nTo intentionally land a commit without triggering a release, use a non-releasing type (`chore:`, `docs:`, etc.) or append `[skip ci]` to the commit subject.\n\n---\n\n## Troubleshooting\n\n### \"The caller does not have permission\" (403)\n\n- Your Google account may not have access to this GTM account/container\n- Service account not added to GTM — see [Service Account Limitations](#service-account-limitations)\n- Check your OAuth scopes on the consent screen\n\n### \"invalid_grant\" or \"Token has been expired or revoked\"\n\n- Re-run `npm run auth:google` to refresh the token file\n- Or set `GOOGLE_REFRESH_TOKEN` directly in `.env` if you prefer env-managed tokens\n- If you're stuck in a loop where Google won't return a `refresh_token`, revoke prior access at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) and re-run the auth script\n\n### \"Write operations are disabled\"\n\n- Set `GTM_MCP_ENABLE_WRITES=true` in your `.env`\n- Restart the server / the desktop app\n\n### Stdio server shows no output\n\n- The stdio server intentionally writes nothing to stdout (stdout is the JSON-RPC channel)\n- Diagnostic output goes to stderr — check your terminal or Claude Desktop logs\n\n### Desktop app: `Error: Electron uninstall` on `npm run dev`\n\n- The Electron binary downloaded but never finished extracting - see the reliable\n  re-extract fix in [apps/desktop/INSTALL.md](apps/desktop/INSTALL.md)\n\n### TypeScript errors on `googleapis` types\n\n- Run `npm install` to ensure all deps are installed\n- The `googleapis` package ships its own types — no `@types/googleapis` needed\n\n---\n\n## TODOs / Known Limitations\n\n- `workspace_resolve_conflict`: The GTM API's resolve_conflict endpoint accepts a full entity body — the exact request body schema is complex. The current implementation passes through the user-supplied JSON; validate it against the entity type before calling.\n- `containers_create`: The `usageContext` enum values may differ slightly by GTM region/version. Refer to the [GTM API docs](https://developers.google.com/tag-manager/api/v2/reference/accounts/containers/create) for the latest allowed values.\n- **Single-identity HTTP auth is a shared secret**: `GTM_MCP_HTTP_AUTH_TOKEN` gates `/mcp` with one bearer token for every client, so it identifies the deployment, not the caller. For per-user identity, set `STYTCH_PROJECT_ID` to enable multi-user mode — see [Security Notes](#security-notes).\n- **HTTP sessions are in-memory**: sessions live in the server process, so horizontal scaling requires sticky sessions. Fine for a single team instance; not yet built for multi-instance load balancing.\n- **Single OAuth identity per deployment — single-identity mode only**: without `STYTCH_PROJECT_ID`, all requests share one Google identity and therefore one Google API quota pool. Heavy multi-user load through one deployment will exhaust it; retries with backoff soften this but don't remove the quota ceiling. Multi-user mode sidesteps it — each member uses their own Google grant and quota.\n\n---\n\n*Built by Samarth Analytics — Swapnil Jaykar & Sarthak Mandage*\n",
  "bytes": 36312,
  "sha": "7d6b9f36abd5c30410da9e472126deb683b490215571f103719de0b15462967d",
  "repo_slug": "samarthanalytics-sj/samarth-analytics-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_samarthanalytics_sj_samarth_gt_4920f145/readme"
}