Samarth GTM MCP Server
A production-ready Model Context Protocol (MCP) server for the Google Tag Manager API v2, built for Samarth Analytics.
Use it through Samarth Desktop - the local Electron app with a chat UI that embeds this server (Quick Start). Full, guarded access to GTM: read workspace contents, create/update tags/triggers/variables, audit implementations, publish versions, and more.
New: browser portal with live QC audit. A white-label, browser-based customer experience lives in
apps/portal/. 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 withnpm run portal:dev.
Table of Contents
- Features
- Quick Start
- Google Cloud OAuth Setup
- Service Account Limitations
- Environment Variables Reference
- Guardrails
- Available Tools
- Cloud Deployment
- Security Notes
- Development
- Releases
- Troubleshooting
Features
- Full GTM API v2 surface — accounts, containers, workspaces, tags, triggers, variables, folders, built-in variables, versions, sync, publish, preview
- 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
- 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 onlyanalytics.readonly; the GA4 Admin write tools are off by default behindGA4_MCP_ENABLE_WRITES/GA4_MCP_ENABLE_DELETESand additionally needanalytics.edit - Automatic pagination — every paginated list tool transparently follows
nextPageTokento return all results, with optionalmaxPages/pageTokenbounds - 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_*) - Two transport modes: stdio (local, for Claude Desktop/Cursor) and Streamable HTTP (cloud/team)
- Guardrails by default: read-only unless explicitly enabled; publish and delete gated separately
- Dry-run mode: simulate all writes without touching the API
confirm=truerequired on all write/delete/publish operations- Audit tool: inspects workspace for common GA4/GTM implementation issues
- Export tool: full workspace dump as structured JSON
- Zod schema validation on all inputs
- Detailed Google API error messages surfaced to the MCP client
Quick Start
The way to run Samarth is the desktop app - a local Electron app with a chat UI that embeds the MCP server in-process. Multi-account Google sign-in, per-account LLM keys (OpenAI / Anthropic / Gemini), secrets in the OS keychain. Nothing to configure by hand: the Google OAuth client and your LLM key are entered in the app on first run.
You need: Node.js 18 or newer, Git, and a free Google "Desktop app" OAuth client - two values you create once in your Google Cloud project (exact click-by-click steps).
git clone https://github.com/samarthanalytics-sj/samarth-analytics-mcp.git
cd samarth-analytics-mcp/apps/desktop
npm install # downloads the Electron binary (~100 MB first time)
npm run dev
The window opens; paste your OAuth client ID + secret and an LLM API key when asked, sign in to Google, and start chatting with your GTM / GA4 setup.
Prefer a config file over typing in the app? Put the OAuth client in a
.env file instead - the app reads it on launch (values typed in the app and
real shell variables always take precedence):
GOOGLE_DESKTOP_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_DESKTOP_CLIENT_SECRET=your-client-secret
Save it as apps/desktop/.env or at the repo root (already gitignored - never
commit it). A repo-root .env using the server's GOOGLE_OAUTH_CLIENT_ID /
GOOGLE_OAUTH_CLIENT_SECRET names works too, so one file can serve both the
app and the MCP server. A packaged install reads .env from its data
directory instead (%APPDATA% on Windows).
Your everyday launch afterwards is just:
cd samarth-analytics-mcp/apps/desktop && npm run dev
Full guide - Windows/macOS specifics, first-run setup, building a real
.exe/.dmg installer, and the Error: Electron uninstall fix:
apps/desktop/INSTALL.md.
Google Cloud OAuth Setup
Step 1: Enable the GTM API
- Go to Google Cloud Console
- Select or create a project
- Navigate to APIs & Services → Library
- Search for "Tag Manager API" and click Enable
- Search for "Google Analytics Admin API" and click Enable (required for the read-only
ga4_*Admin tools) - Search for "Google Analytics Data API" and click Enable (required for
ga4_run_report/ga4_run_realtime_report)
Step 2: Create OAuth 2.0 Credentials
- Go to APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID
- Choose application type:
- Desktop app — simplest for local stdio use (no redirect URI needed)
- Web application — for the HTTP server (add your redirect URI)
- Download the JSON or copy the Client ID and Client Secret
- Add to
.env:GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret GOOGLE_REDIRECT_URI=http://localhost:3001/oauth/callback
Step 3: Configure OAuth Consent Screen
- Go to APIs & Services → OAuth consent screen
- Choose External (or Internal if you have Google Workspace)
- Fill in App name, support email
- Add scopes:
https://www.googleapis.com/auth/tagmanager.readonlyhttps://www.googleapis.com/auth/tagmanager.edit.containershttps://www.googleapis.com/auth/tagmanager.edit.containerversionshttps://www.googleapis.com/auth/tagmanager.manage.accountshttps://www.googleapis.com/auth/tagmanager.manage.usershttps://www.googleapis.com/auth/tagmanager.publishhttps://www.googleapis.com/auth/analytics.readonly(read-only GA4 Admin and Data API tools)https://www.googleapis.com/auth/analytics.edit(GA4 Admin write tools — gated byGA4_MCP_ENABLE_WRITES)https://www.googleapis.com/auth/analytics.manage.users(GA4 access-binding write tools)
- Add your Google account as a test user (while the app is in "testing" mode)
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.
Step 4: Run OAuth Setup
npm run auth:google
Or, if you prefer the older paste-the-code helper:
npm run oauth:setup
Service Account Limitations
Short version: Service accounts do NOT work with GTM by default. Use OAuth 2.0.
The 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.
Option A: Add the service account as a GTM user (simplest)
If you still want to use a service account:
- Get the service account email (e.g.,
my-sa@project.iam.gserviceaccount.com) - In GTM, go to Admin → User Management at the account or container level
- Add the service account email with the appropriate role (Read, Edit, Approve, Publish)
- Set
GOOGLE_SERVICE_ACCOUNT_KEY_FILE=/path/to/key.jsonin.env
Caveats: This only works if the GTM container is associated with a Google account, not a Google Workspace that restricts external sharing.
Option B: Domain-Wide Delegation (Google Workspace only)
For Google Workspace organizations:
- Create a service account with a JSON key
- Enable Domain-Wide Delegation on the service account in Google Cloud Console
- In Google Workspace Admin Console → Security → API Controls → Domain-wide Delegation:
- Add the service account client ID
- Add scopes:
https://www.googleapis.com/auth/tagmanager.edit.containers(and others as needed)
- In
.env, set:GOOGLE_SERVICE_ACCOUNT_KEY_FILE=/path/to/key.json - The server will impersonate the user automatically if you set a subject in
buildGoogleAuth()
Caveats: Requires a paid Google Workspace account. Only available for your own domain.
Environment Variables Reference
| Variable | Default | Description |
|---|---|---|
GOOGLE_OAUTH_CLIENT_ID | — | OAuth client ID (preferred). Falls back to GOOGLE_CLIENT_ID. |
GOOGLE_OAUTH_CLIENT_SECRET | — | OAuth client secret (preferred). Falls back to GOOGLE_CLIENT_SECRET. |
GOOGLE_OAUTH_REDIRECT_URI | http://localhost:3001/oauth/callback | OAuth redirect URI. Falls back to GOOGLE_REDIRECT_URI. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI | — | Legacy names, still supported. |
SAMARTH_GOOGLE_OAUTH_CLIENT_ID | — | Hosted-only. Samarth-owned public OAuth client. Takes precedence over the self-hosted vars when set. |
SAMARTH_GOOGLE_OAUTH_CLIENT_SECRET | — | Hosted-only. Inject from your platform secret manager. Never commit. |
GOOGLE_ACCESS_TOKEN | — | Current OAuth access token. Env vars take precedence over the token file. |
GOOGLE_REFRESH_TOKEN | — | OAuth refresh token (long-lived). Env vars take precedence over the token file. |
GTM_MCP_TOKEN_FILE | ./.gtm-mcp-tokens.json | Path to the local OAuth token file written by npm run auth:google (gitignored). |
GOOGLE_SERVICE_ACCOUNT_KEY_FILE | — | Path to service account JSON key (see limitations above) |
GTM_MCP_TRANSPORT | stdio | Transport: stdio or http |
GTM_MCP_HTTP_PORT | 3001 | HTTP server port (http transport only; falls back to PORT) |
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. |
GTM_MCP_HTTP_ALLOW_UNAUTHENTICATED | false | Local-dev opt-in to start without auth. Binds loopback only unless GTM_MCP_HTTP_HOST overrides. |
GTM_MCP_HTTP_HOST | — | Bind host. Defaults to loopback when unauthenticated, all interfaces when authenticated. |
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). |
STYTCH_SECRET | — | Stytch project secret (server-only). Required when STYTCH_PROJECT_ID is set — the server exits without it. |
STYTCH_PUBLIC_TOKEN | — | Publishable token powering the /oauth/authorize page. Not a secret. |
GTM_MCP_PUBLIC_URL | http://localhost:<port> | This server's public origin, advertised in the OAuth Protected Resource Metadata document. |
GTM_MCP_ENABLE_WRITES | false | Allow create/update operations |
GTM_MCP_ENABLE_PUBLISH | false | Allow publish operations |
GTM_MCP_ENABLE_DELETES | false | Allow delete operations |
DRY_RUN | false | Simulate all writes without calling the API |
GTM_MCP_RETRY_MAX | 3 | Retry attempts for transient read failures (408/429/5xx, network). 0 disables retries. Mutations are never auto-retried. |
GTM_MCP_RETRY_MAX_DELAY_MS | 30000 | Cap on a single backoff sleep (exponential backoff with jitter) |
GTM_MCP_RETRY_TOTAL_TIMEOUT_MS | 60000 | Cap on total wall time from first request to last retry |
Guardrails
The server enforces three independent guardrails in addition to the confirm=true requirement:
| Guardrail | Env Variable | What it gates |
|---|---|---|
| Write guard | GTM_MCP_ENABLE_WRITES=true | All create and update operations |
| Delete guard | GTM_MCP_ENABLE_DELETES=true | All delete operations |
| Publish guard | GTM_MCP_ENABLE_PUBLISH=true | All version publish operations |
| Dry run | DRY_RUN=true | Simulate without API calls (overrides all) |
confirm=true is always required on write/delete/publish tools regardless of env settings. This prevents accidental modifications even when guardrails are enabled.
Recommended Configurations
Read-only exploration (default — safe for sharing with team):
GTM_MCP_ENABLE_WRITES=false
GTM_MCP_ENABLE_PUBLISH=false
GTM_MCP_ENABLE_DELETES=false
Development workspace edits (no publishing):
GTM_MCP_ENABLE_WRITES=true
GTM_MCP_ENABLE_PUBLISH=false
GTM_MCP_ENABLE_DELETES=false
Full access (use with care):
GTM_MCP_ENABLE_WRITES=true
GTM_MCP_ENABLE_PUBLISH=true
GTM_MCP_ENABLE_DELETES=true
Available Tools
Accounts
| Tool | Description |
|---|---|
accounts_list | List all accessible GTM accounts |
accounts_get | Get a specific account |
Containers
| Tool | Description |
|---|---|
containers_list | List containers in an account (auto-paginated) |
containers_get | Get a specific container |
containers_create | ✏️ Create a new container |
containers_snippet | Get the GTM installation snippet for a container |
containers_lookup | Look up a container by linked destination/tag ID (e.g. G-XXXX) |
containers_combine | ✏️ Combine (merge) another container into this one |
containers_move_tag_id | ✏️ Move a Tag ID out into a new container |
Destinations
| Tool | Description |
|---|---|
destinations_list | List linked destinations (Google tags / GA4) for a container |
destinations_get | Get a specific destination |
destinations_link | ✏️ Link a destination to a container |
Workspaces
| Tool | Description |
|---|---|
workspaces_list | List workspaces in a container (auto-paginated) |
workspaces_get | Get a specific workspace |
workspaces_create | ✏️ Create a new workspace |
workspace_get_status | Review the change diff (changed entities + merge conflicts) before versioning |
workspace_sync | ✏️ Sync workspace to latest container version |
workspace_resolve_conflict | ✏️ Resolve a merge conflict |
workspace_quick_preview | Generate a preview link (read-safe) |
workspace_create_version_and_publish | 🚀 Create version + publish in one step |
Tags
| Tool | Description |
|---|---|
tags_list | List all tags in a workspace |
tags_get | Get a specific tag |
tags_create | ✏️ Create a tag |
tags_update | ✏️ Update a tag |
tags_delete | 🗑️ Delete a tag |
Triggers
| Tool | Description |
|---|---|
triggers_list | List all triggers |
triggers_get | Get a specific trigger |
triggers_create | ✏️ Create a trigger |
triggers_update | ✏️ Update a trigger |
triggers_delete | 🗑️ Delete a trigger |
Variables
| Tool | Description |
|---|---|
variables_list | List all user-defined variables |
variables_get | Get a specific variable |
variables_create | ✏️ Create a variable |
variables_update | ✏️ Update a variable |
variables_delete | 🗑️ Delete a variable |
Folders
| Tool | Description |
|---|---|
folders_list | List all folders |
folders_get | Get a specific folder |
folders_entities | List entities in a folder (auto-paginated) |
folders_create | ✏️ Create a folder |
folders_update | ✏️ Update a folder |
folders_delete | 🗑️ Delete a folder |
folders_move_entities | ✏️ Move entities into a folder |
Built-In Variables
| Tool | Description |
|---|---|
built_in_variables_list | List enabled built-in variables |
built_in_variables_enable | ✏️ Enable built-in variables |
built_in_variables_disable | 🗑️ Disable built-in variables |
built_in_variables_revert | ✏️ Revert a built-in variable to base version |
Versions
| Tool | Description |
|---|---|
versions_list | List version headers |
versions_get | Get a version (pass "live" for current live version) |
versions_create | ✏️ Create a checkpoint version from workspace |
versions_set_latest | ✏️ Set a version as latest |
versions_publish | 🚀 Publish a specific version |
versions_undelete | ✏️ Undelete a version |
versions_delete | 🗑️ Delete a version |
Environments
| Tool | Description |
|---|---|
environments_list | List environments in a container (auto-paginated) |
environments_get | Get a specific environment |
environments_create | ✏️ Create an environment |
environments_update | ✏️ Update an environment |
environments_reauthorize | 🚀 Re-generate the environment authorization token (high-impact) |
environments_delete | 🗑️ Delete an environment |
User Permissions (account-level)
| Tool | Description |
|---|---|
user_permissions_list | List user permissions for an account (auto-paginated) |
user_permissions_get | Get a specific user permission |
user_permissions_create | ✏️ Grant a user account/container access |
user_permissions_update | ✏️ Update a user's access levels |
user_permissions_delete | 🗑️ Revoke a user's access |
Server-Side & Advanced Container Resources
These are workspace-scoped resources from GTM API v2. Create/update accept the full
resource as a JSON string (bodyJson) since their bodies are deeply nested. Each
supports *_list (auto-paginated), *_get, *_create ✏️, *_update ✏️, *_delete 🗑️,
and (except gtag_config) *_revert ✏️.
| Resource | Tools | Notes |
|---|---|---|
| Clients | clients_* | Server container request clients |
| Transformations | transformations_* | Server container event transformations |
| Zones | zones_* | Zone delegation |
| Templates | templates_* | Custom / gallery-installed templates |
| Gtag Config | gtag_config_* | Google tag (gtag) configuration — no revert |
Analytics & Export
| Tool | Description |
|---|---|
audit_container | Inspect workspace for analytics issues |
export_container | Export workspace as structured JSON |
GA4 Admin (read-only)
Read-only wrappers over the Google Analytics Admin API (v1beta, with a single
v1alpha call for enhanced measurement). These never write, update, or delete GA4
resources and require no confirm flag. They power the senior audit framework's
GA4_ADMIN checks (custom dimensions/metrics, data streams & measurement IDs, data
retention, enhanced measurement, key events, Google Ads links).
Requires the https://www.googleapis.com/auth/analytics.readonly scope and the
Google Analytics Admin API enabled in your Google Cloud project. A 403 mentioning
scope means you should re-run npm run auth:google.
| Tool | Description |
|---|---|
ga4_account_summaries_list | List GA4 accounts + their property summaries (best discovery entry point) |
ga4_properties_list | List properties under a parent account (display name, time zone, currency, service level) |
ga4_property_get | Get a single property by ID |
ga4_data_streams_list | List data streams (web/Android/iOS) incl. web measurement IDs |
ga4_enhanced_measurement_get | Get enhanced measurement settings for a web data stream (v1alpha) |
ga4_custom_dimensions_list | List custom dimensions (parameter, scope) |
ga4_custom_metrics_list | List custom metrics (parameter, unit, scope) |
ga4_data_retention_get | Get event data-retention settings |
ga4_key_events_list | List key events (formerly "conversion events" — current Admin naming) |
ga4_google_ads_links_list | List Google Ads links (customer ID, auto-tagging/ads-personalization flags) |
Accepts either a bare numeric ID (123456789) or the fully-qualified form
(properties/123456789, accounts/123456) wherever a property/account is required.
Documented limitations (not exposed by the GA4 Admin API v1beta, so intentionally not implemented rather than faked):
- Internal-traffic / unwanted-referral data filters — no public
dataFilterscollection; configured per data stream. - Referral exclusions — no dedicated Admin API resource.
- Channel groups and audiences — exist only on the v1alpha surface and are out of scope for this read-only v1beta set.
GA4 Data API (read-only reporting)
Read-only wrappers over the Google Analytics Data API (v1beta). They never
write and require no confirm flag. Use them to reconcile intent vs. reality —
comparing the events a container is configured to send against the events GA4
actually reports (zero reported activity for a configured event is a red flag).
These use the same https://www.googleapis.com/auth/analytics.readonly scope
as the GA4 Admin tools, so no extra consent is needed. Enable the Google
Analytics Data API in your Google Cloud project.
| Tool | Description |
|---|---|
ga4_run_report | Run a report over a date range (dimensions + metrics, e.g. eventCount by eventName); supports limit, offset, and ordering |
ga4_run_realtime_report | Run a Realtime report (events in roughly the last 30 minutes) for live QA |
Documented gaps (intentionally not exposed rather than faked): pivot reports, cohorts, and funnels.
Pagination
All list tools backed by paginated GTM endpoints (accounts_*-scoped containers,
workspaces, tags, triggers, variables, folders, folder entities, environments,
user permissions, clients, transformations, zones, templates, gtag configs)
auto-follow pagination and return all results by default. Optional arguments:
maxPages— cap the number of API pages fetched (default 50). If more pages remain, the response includes"truncated": trueand anextPageToken.pageToken— resume from a previous truncated result.
Non-truncated responses keep the original { <key>: [...], count } shape unchanged.
Two tools differ, because one list key does not describe what they return:
folders_entitiesreturns three parallel collections (tag,trigger,variable, always present, empty when the folder has none) plus acountsobject, and addstruncated/nextPageTokenonly when the page ceiling was hit.export_containerpages five collections independently, so it takesmaxPages(applied per collection) but nopageToken. A short export is markedincomplete: truewithtruncatedCollections, per-collectionnextPageTokensand awarning— in every format, including the defaultsummary.
Legend: ✏️ requires GTM_MCP_ENABLE_WRITES=true | 🗑️ requires GTM_MCP_ENABLE_DELETES=true | 🚀 requires GTM_MCP_ENABLE_PUBLISH=true
All ✏️ 🗑️ 🚀 tools also require confirm: true in the tool arguments.
Cloud Deployment
Transport
For cloud deployments, use GTM_MCP_TRANSPORT=http. The server exposes:
POST /mcp— Streamable HTTP MCP endpointGET /mcp— SSE stream for existing sessionsDELETE /mcp— Session terminationGET /health— Health check
There 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.
Connecting Remote Clients
Clients that support Streamable HTTP can connect directly to the /mcp endpoint. For clients that only support stdio (like Claude Desktop), use mcp-remote as a proxy:
{
"mcpServers": {
"samarth-gtm-remote": {
"command": "npx",
"args": ["mcp-remote@next", "https://your-server.com/mcp"]
}
}
}
Vercel
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.
For Vercel, export the Express app as a serverless handler:
// api/mcp.ts
export default app; // where app is the Express instance
Set env vars in Vercel Dashboard → Settings → Environment Variables.
Render
- Create a new Web Service in Render
- Connect your GitHub repo
- Build command:
npm install && npm run build - Start command:
GTM_MCP_TRANSPORT=http node dist/index.js - Add environment variables in Render Dashboard
- Important: Set
RENDER=trueenv var and ensure your health check hits/health
Render supports persistent long-lived connections — recommended for SSE/streaming.
Fly.io
fly launch
fly secrets set GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx
fly secrets set GTM_MCP_TRANSPORT=http GTM_MCP_HTTP_PORT=3001
fly deploy
Fly.io has no request timeout limitations and supports persistent WebSocket/SSE connections. Recommended for production.
Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
ENV GTM_MCP_TRANSPORT=http
ENV GTM_MCP_HTTP_PORT=3001
EXPOSE 3001
CMD ["node", "dist/index.js"]
Security Notes
-
Never commit
.env— it contains OAuth tokens..envis already in.gitignore. -
Rotate tokens regularly — OAuth refresh tokens are long-lived but can be revoked. Revoke at myaccount.google.com/permissions.
-
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.readonlyonly. -
Cloud deployment: Store secrets in your platform's secret manager (Render Secrets, Fly.io Secrets, Vercel Env Vars), never in code or Docker images.
-
HTTP transport:
/mcphas two built-in auth modes —GTM_MCP_HTTP_AUTH_TOKEN(one shared bearer token, identifies the deployment) andSTYTCH_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=trueoverrides that for local development and binds loopback only. -
Publish guard: Keep
GTM_MCP_ENABLE_PUBLISH=falseunless you explicitly intend to publish from an AI client. Publishing incorrect tags to production is the highest-risk operation. -
Audit logs: The server logs all session events to stderr. Pipe to a logging service in production.
Development
# Install dependencies
npm install
# TypeScript type check (no emit)
npm run typecheck
# Build
npm run build
# Watch mode
npm run build:watch
# Run dev server (stdio, with hot-reload)
npm run dev
# Run HTTP dev server
npm run dev:http
# Tests (run `npm run build` first — some suites test the compiled dist)
npm test
# Smoke test: server boots and answers tools/list
npm run smoke -- --mcp dist/index.js
# Full-surface smoke test: invokes ALL registered tools with a sanitized,
# credential-free env — every handler must respond cleanly (no crash/hang)
npm run smoke:all
# MCP Inspector (interactive tool debugging)
npm run inspector
Project Structure
samarth-gtm-mcp/
├── src/
│ ├── index.ts # Entry point — stdio/HTTP transport setup
│ ├── server.ts # MCP server factory + tool registration
│ ├── auth/
│ │ └── googleAuth.ts # OAuth2 / service account auth
│ ├── tools/
│ │ ├── index.ts # Tool registration aggregator
│ │ ├── accounts.ts # accounts/list, accounts/get
│ │ ├── containers.ts # containers/list/get/create
│ │ ├── workspaces.ts # workspaces + sync/resolve_conflict
│ │ ├── tags.ts # tags CRUD
│ │ ├── triggers.ts # triggers CRUD
│ │ ├── variables.ts # variables CRUD
│ │ ├── folders.ts # folders CRUD + move_entities
│ │ ├── builtInVariables.ts # enable/disable/revert built-ins
│ │ ├── versions.ts # versions list/get/create/publish/delete
│ │ ├── publish.ts # quick_preview, versions_publish, create+publish
│ │ ├── audit.ts # audit_container analytics checks
│ │ ├── export.ts # export_container JSON dump
│ │ ├── environments.ts # environments CRUD + reauthorize
│ │ ├── userPermissions.ts # account-level user permissions
│ │ ├── serverSide.ts # clients, transformations, zones, templates, gtag config
│ │ ├── ga4Admin.ts # read-only GA4 Admin tools (ga4_*)
│ │ └── ga4Data.ts # read-only GA4 Data API reporting
│ ├── utils/
│ │ ├── guardrails.ts # Guardrail enforcement, error formatting
│ │ ├── gtmClient.ts # googleapis GTM v2 client factory
│ │ ├── ga4Client.ts # GA4 Admin/Data client factories
│ │ ├── apiRetry.ts # retry/backoff config (429/5xx, reads only)
│ │ ├── pagination.ts # transparent nextPageToken following
│ │ ├── schemas.ts # shared Zod input schemas
│ │ └── toolResponse.ts # standard tool result shaping
│ ├── types/
│ │ ├── gtm.ts # GTM API type definitions
│ │ └── index.ts
│ ├── scripts/
│ │ ├── auth-google.ts # Browser-based OAuth onboarding (`npm run auth:google`)
│ │ └── oauth-setup.ts # Interactive OAuth token helper (legacy paste-the-code flow)
│ └── __tests__/
│ ├── guardrails.node.test.mjs # guardrails + buildPath
│ ├── auth.node.test.mjs # env/auth resolution + token file paths
│ ├── pagination.node.test.mjs # paginate/buildListResult
│ ├── ga4Admin.node.test.mjs # GA4 tool registration (tests compiled dist)
│ └── apiRetry.node.test.mjs # retry/backoff config (tests compiled dist)
├── scripts/
│ ├── smoke-test.mjs # health probe: portal endpoints + MCP tools/list
│ └── smoke-all-tools.mjs # invokes all tools with sanitized env
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
Releases
Releases are fully automated via semantic-release and GitHub Actions. Every push to main triggers the release.yml workflow, which:
- Installs dependencies, type-checks, builds, and runs tests.
- Inspects commits since the last tag using the Conventional Commits spec.
- Determines the next semantic version (
MAJOR.MINOR.PATCH). - Updates
CHANGELOG.mdand bumps theversioninpackage.json/package-lock.json. - Commits those files back to
mainwithchore(release): x.y.z [skip ci](the[skip ci]marker prevents an infinite release loop). - Creates a Git tag (
vX.Y.Z) and a GitHub Release with auto-generated notes.
The 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.
Conventional Commit Examples
Commit messages drive the version bump:
| Commit prefix | Effect | Example |
|---|---|---|
fix: | Patch release (x.y.Z) | fix: handle empty workspace in audit tool |
feat: | Minor release (x.Y.0) | feat: add bulk tag import tool |
perf: | Patch release | perf: cache GTM client between tool calls |
docs: / chore: / refactor: / test: / style: / ci: / build: | No release | docs: clarify OAuth setup steps |
BREAKING CHANGE: footer or ! after type | Major release (X.0.0) | see below |
Breaking change examples
feat!: drop support for Node.js 18
BREAKING CHANGE: minimum required Node version is now 20.
or:
refactor(auth): rename GOOGLE_REFRESH_TOKEN env var
BREAKING CHANGE: GOOGLE_REFRESH_TOKEN is now GTM_GOOGLE_REFRESH_TOKEN.
Update your .env file accordingly.
Dry run locally
To preview what the next release would look like without publishing:
GITHUB_TOKEN=<a-token-with-no-perms-is-fine-for-dry-run> \
npx semantic-release --dry-run --no-ci
Manual release skip
To intentionally land a commit without triggering a release, use a non-releasing type (chore:, docs:, etc.) or append [skip ci] to the commit subject.
Troubleshooting
"The caller does not have permission" (403)
- Your Google account may not have access to this GTM account/container
- Service account not added to GTM — see Service Account Limitations
- Check your OAuth scopes on the consent screen
"invalid_grant" or "Token has been expired or revoked"
- Re-run
npm run auth:googleto refresh the token file - Or set
GOOGLE_REFRESH_TOKENdirectly in.envif you prefer env-managed tokens - If you're stuck in a loop where Google won't return a
refresh_token, revoke prior access at myaccount.google.com/permissions and re-run the auth script
"Write operations are disabled"
- Set
GTM_MCP_ENABLE_WRITES=truein your.env - Restart the server / the desktop app
Stdio server shows no output
- The stdio server intentionally writes nothing to stdout (stdout is the JSON-RPC channel)
- Diagnostic output goes to stderr — check your terminal or Claude Desktop logs
Desktop app: Error: Electron uninstall on npm run dev
- The Electron binary downloaded but never finished extracting - see the reliable re-extract fix in apps/desktop/INSTALL.md
TypeScript errors on googleapis types
- Run
npm installto ensure all deps are installed - The
googleapispackage ships its own types — no@types/googleapisneeded
TODOs / Known Limitations
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.containers_create: TheusageContextenum values may differ slightly by GTM region/version. Refer to the GTM API docs for the latest allowed values.- Single-identity HTTP auth is a shared secret:
GTM_MCP_HTTP_AUTH_TOKENgates/mcpwith one bearer token for every client, so it identifies the deployment, not the caller. For per-user identity, setSTYTCH_PROJECT_IDto enable multi-user mode — see Security Notes. - 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.
- 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.
Built by Samarth Analytics — Swapnil Jaykar & Sarthak Mandage