Samarth GTM MCP Server
OfficialProvides read-only tools for GA4 Admin (account, property, data stream management) and GA4 Data API reporting (run reports, run realtime reports) for intent-vs-reality reconciliation.
Provides tools for managing Google Tag Manager accounts, containers, workspaces, tags, triggers, variables, folders, built-in variables, versions, environments, user permissions, and more. Includes audit, export, and publish capabilities with guardrails.
Samarth GTM MCP Server
A production-ready Model Context Protocol (MCP) server for the Google Tag Manager API v2, built for Samarth Analytics.
Gives Claude Desktop, Cursor, Claude Code, and any MCP-compatible client 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
Related MCP server: gitlab-mcp
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
Read-only GA4 coverage — GA4 Admin tools (
ga4_*) plus GA4 Data API reporting (ga4_run_report,ga4_run_realtime_report) for intent-vs-reality reconciliation, all under a singleanalytics.readonlyscopeAutomatic pagination — every paginated list tool transparently follows
nextPageTokento return all results, with optionalmaxPages/pageTokenboundsRetry 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 operationsAudit 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
Prerequisites
Node.js ≥ 18
A Google Cloud project with the Tag Manager API enabled
A Google account with access to your GTM containers
Run with npx (no clone needed)
Once the package is on npm, the fastest path is:
# One-time OAuth onboarding (opens your browser; writes a local token file)
GOOGLE_OAUTH_CLIENT_ID=... GOOGLE_OAUTH_CLIENT_SECRET=... npx -y -p samarth-gtm-mcp samarth-gtm-auth
# Run the server (stdio)
npx -y samarth-gtm-mcpIn MCP client configs, use "command": "npx", "args": ["-y", "samarth-gtm-mcp"].
Install & Build (from source)
git clone https://github.com/samarthanalytics-sj/samarth-analytics-mcp.git
cd samarth-analytics-mcp
npm install
cp .env.example .env
# Edit .env — at minimum add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
npm run buildOne-time OAuth Setup
The fastest way is the new browser-based onboarding script:
npm run auth:googleThis will:
Start a tiny local callback server on
http://localhost:3001/oauth/callback.Open the Google authorization URL in your default browser (or print it if no browser is available).
Capture the redirect, exchange the code for tokens, and save them to
./.gtm-mcp-tokens.json(mode0600, already in.gitignore).
You do not need to paste tokens into .env afterwards — the server reads them from the token file automatically. If you prefer to manage tokens by hand, the legacy paste-the-code helper is still available as npm run oauth:setup.
See Friendly Google Auth Options for the three supported setup paths (hosted, local dev, advanced).
Verify it works
# Test stdio server starts (Ctrl+C to exit)
npm start
# Or use the MCP inspector
npm run inspectorFriendly Google Auth Options
Google's Tag Manager API requires an OAuth-enabled Google Cloud project — somebody has to own one. There is no anonymous, key-free way to call the GTM API. What this MCP can do is hide that complexity behind a hosted backend, while still giving developers and teams the option to run everything themselves.
There are three supported paths. Pick the one that fits your situation.
1. Easiest — Samarth-hosted MCP (recommended for non-developers)
Status: planned. This section documents the intended UX; the hosted endpoint is owned by Samarth Analytics and announced separately.
If you don't want to manage a Google Cloud project at all:
Point your MCP client at the Samarth-hosted endpoint (e.g.
https://mcp.samarthanalytics.com/mcpviamcp-remote).Sign in with Google in the browser when prompted.
Done — the hosted server uses the Samarth-owned, Google-verified OAuth app. The client secret lives only on the hosted backend; nothing sensitive ever ships to your machine.
Trade-off: you trust the Samarth-hosted backend to broker your Google tokens. It only ever requests the GTM scopes listed below.
2. Local developer — self-hosted with your own OAuth client
Best if you're comfortable in Google Cloud Console and want full control.
Create an OAuth 2.0 Client ID (type: Desktop app or Web application) in your own Google Cloud project. See Google Cloud OAuth Setup below for the exact steps.
Copy the client ID and secret into
.env:GOOGLE_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_OAUTH_CLIENT_SECRET=your-client-secret GOOGLE_OAUTH_REDIRECT_URI=http://localhost:3001/oauth/callback(The legacy
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETnames still work.)Run the browser-based onboarding flow:
npm run auth:googleIt boots a local callback server, opens the consent screen, and writes tokens to
./.gtm-mcp-tokens.json(gitignored,0600). The token file path is configurable viaGTM_MCP_TOKEN_FILE.Start the MCP server (
npm startor via Claude Desktop / Cursor / Claude Code). Tokens are picked up from the file automatically — no need to paste anything into.env.
Trade-off: you maintain your own Google Cloud project and re-authorise every 7 days while the OAuth app is in "Testing" mode (or publish + verify it for permanent tokens).
3. Advanced / team — Samarth-owned OAuth on your own infra
Best for agencies and teams that want a hosted server with their own auth/IAM in front of it.
Deploy the MCP HTTP server (Render, Fly.io, Docker — see Cloud Deployment).
Set the Samarth-owned (or your team-owned) OAuth client on the server only:
SAMARTH_GOOGLE_OAUTH_CLIENT_ID=public-client-id SAMARTH_GOOGLE_OAUTH_CLIENT_SECRET=secret-injected-from-secret-managerThe secret must come from your platform's secret manager (Render Secrets, Fly Secrets, Vault, etc.). It is never committed to this repo and never shipped to client machines.
Front the
/mcpendpoint with your own auth layer (API key header, IP allowlist, SSO proxy). The MCP server itself has no built-in user auth — see Security Notes.Users connect with their MCP client and never see Google Cloud Console.
Trade-off: you own the operational burden (Google API quota, OAuth app verification, secret rotation, user provisioning) in exchange for a friendly UX.
Why we can't ship a "no-setup" client secret
Public OAuth clients distributed in source form (or pre-baked into an installer) are not secure: anyone can extract the secret and impersonate the app, leading to Google revoking it. This repo therefore:
Never hardcodes a client secret.
Treats
SAMARTH_GOOGLE_OAUTH_CLIENT_SECRETas runtime-injected on the hosted backend only.Defaults the local flow (option 2) to your own OAuth client, which keeps the secret on your machine.
Scopes requested in every path:
https://www.googleapis.com/auth/tagmanager.readonly
https://www.googleapis.com/auth/tagmanager.edit.containers
https://www.googleapis.com/auth/tagmanager.edit.containerversions
https://www.googleapis.com/auth/tagmanager.manage.accounts
https://www.googleapis.com/auth/tagmanager.manage.users
https://www.googleapis.com/auth/tagmanager.publish
https://www.googleapis.com/auth/analytics.readonlyThese are the least-privilege scopes needed to cover the server's full tool surface. The final scope, analytics.readonly, powers both the read-only GA4 Admin tools (ga4_*) and the read-only GA4 Data API reporting tools (ga4_run_report, ga4_run_realtime_report) and grants no write access to Google Analytics. Read-only deployments can re-run npm run auth:google after removing the edit.*, manage.*, and publish scopes from your OAuth consent screen; keep tagmanager.readonly and analytics.readonly for full read coverage.
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)
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:googleOr, if you prefer the older paste-the-code helper:
npm run oauth:setupService 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.jsonThe 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 |
| — | OAuth client ID (preferred). Falls back to |
| — | OAuth client secret (preferred). Falls back to |
|
| OAuth redirect URI. Falls back to |
| — | Legacy names, still supported. |
| — | Hosted-only. Samarth-owned public OAuth client. Takes precedence over the self-hosted vars when set. |
| — | Hosted-only. Inject from your platform secret manager. Never commit. |
| — | Current OAuth access token. Env vars take precedence over the token file. |
| — | OAuth refresh token (long-lived). Env vars take precedence over the token file. |
|
| Path to the local OAuth token file written by |
| — | Path to service account JSON key (see limitations above) |
|
| Transport: |
|
| HTTP server port (http transport only; falls back to |
| — | Bearer token gating |
|
| Allow create/update operations |
|
| Allow publish operations |
|
| Allow delete operations |
|
| Simulate all writes without calling the API |
|
| Retry attempts for transient read failures (408/429/5xx, network). |
|
| Cap on a single backoff sleep (exponential backoff with jitter) |
|
| Cap on total wall time from first request to last retry |
| — | Optional default accountId |
| — | Optional default containerId |
| — | Optional default workspaceId |
Guardrails
The server enforces three independent guardrails in addition to the confirm=true requirement:
Guardrail | Env Variable | What it gates |
Write guard |
| All |
Delete guard |
| All |
Publish guard |
| All version publish operations |
Dry run |
| 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=falseDevelopment workspace edits (no publishing):
GTM_MCP_ENABLE_WRITES=true
GTM_MCP_ENABLE_PUBLISH=false
GTM_MCP_ENABLE_DELETES=falseFull access (use with care):
GTM_MCP_ENABLE_WRITES=true
GTM_MCP_ENABLE_PUBLISH=true
GTM_MCP_ENABLE_DELETES=trueAvailable Tools
Accounts
Tool | Description |
| List all accessible GTM accounts |
| Get a specific account |
Containers
Tool | Description |
| List containers in an account (auto-paginated) |
| Get a specific container |
| ✏️ Create a new container |
| Get the GTM installation snippet for a container |
| Look up a container by linked destination/tag ID (e.g. |
| ✏️ Combine (merge) another container into this one |
| ✏️ Move a Tag ID out into a new container |
Destinations
Tool | Description |
| List linked destinations (Google tags / GA4) for a container |
| Get a specific destination |
| ✏️ Link a destination to a container |
Workspaces
Tool | Description |
| List workspaces in a container (auto-paginated) |
| Get a specific workspace |
| ✏️ Create a new workspace |
| Review the change diff (changed entities + merge conflicts) before versioning |
| ✏️ Sync workspace to latest container version |
| ✏️ Resolve a merge conflict |
| Generate a preview link (read-safe) |
| 🚀 Create version + publish in one step |
Tags
Tool | Description |
| List all tags in a workspace |
| Get a specific tag |
| ✏️ Create a tag |
| ✏️ Update a tag |
| 🗑️ Delete a tag |
Triggers
Tool | Description |
| List all triggers |
| Get a specific trigger |
| ✏️ Create a trigger |
| ✏️ Update a trigger |
| 🗑️ Delete a trigger |
Variables
Tool | Description |
| List all user-defined variables |
| Get a specific variable |
| ✏️ Create a variable |
| ✏️ Update a variable |
| 🗑️ Delete a variable |
Folders
Tool | Description |
| List all folders |
| Get a specific folder |
| List entities in a folder |
| ✏️ Create a folder |
| ✏️ Update a folder |
| 🗑️ Delete a folder |
| ✏️ Move entities into a folder |
Built-In Variables
Tool | Description |
| List enabled built-in variables |
| ✏️ Enable built-in variables |
| 🗑️ Disable built-in variables |
| ✏️ Revert a built-in variable to base version |
Versions
Tool | Description |
| List version headers |
| Get a version (pass "live" for current live version) |
| ✏️ Create a checkpoint version from workspace |
| ✏️ Set a version as latest |
| 🚀 Publish a specific version |
| ✏️ Undelete a version |
| 🗑️ Delete a version |
Environments
Tool | Description |
| List environments in a container (auto-paginated) |
| Get a specific environment |
| ✏️ Create an environment |
| ✏️ Update an environment |
| 🚀 Re-generate the environment authorization token (high-impact) |
| 🗑️ Delete an environment |
User Permissions (account-level)
Tool | Description |
| List user permissions for an account (auto-paginated) |
| Get a specific user permission |
| ✏️ Grant a user account/container access |
| ✏️ Update a user's access levels |
| 🗑️ 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 |
| Server container request clients |
Transformations |
| Server container event transformations |
Zones |
| Zone delegation |
Templates |
| Custom / gallery-installed templates |
Gtag Config |
| Google tag (gtag) configuration — no |
Analytics & Export
Tool | Description |
| Inspect workspace for analytics issues |
| 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 |
| List GA4 accounts + their property summaries (best discovery entry point) |
| List properties under a parent account (display name, time zone, currency, service level) |
| Get a single property by ID |
| List data streams (web/Android/iOS) incl. web measurement IDs |
| Get enhanced measurement settings for a web data stream (v1alpha) |
| List custom dimensions (parameter, scope) |
| List custom metrics (parameter, unit, scope) |
| Get event data-retention settings |
| List key events (formerly "conversion events" — current Admin naming) |
| 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 |
| Run a report over a date range (dimensions + metrics, e.g. |
| 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, 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.
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.
Claude Desktop Config
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"samarth-gtm": {
"command": "node",
"args": ["/absolute/path/to/samarth-gtm-mcp/dist/index.js"],
"env": {
"GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
"GOOGLE_CLIENT_SECRET": "your-client-secret",
"GOOGLE_REFRESH_TOKEN": "your-refresh-token",
"GTM_MCP_ENABLE_WRITES": "false",
"GTM_MCP_ENABLE_PUBLISH": "false",
"GTM_MCP_ENABLE_DELETES": "false"
}
}
}
}Tip: Set the env variables in .env and remove them from the config to keep credentials out of version control. The server loads .env automatically via dotenv.
After editing, restart Claude Desktop.
Cursor Config
In Cursor, go to Settings → MCP and add:
{
"mcpServers": {
"samarth-gtm": {
"command": "node",
"args": ["/absolute/path/to/samarth-gtm-mcp/dist/index.js"]
}
}
}Make sure your .env file is present in the project root so dotenv picks it up.
Claude Code Config
Add to .claude/mcp_config.json in your project root:
{
"mcpServers": {
"samarth-gtm": {
"command": "node",
"args": ["/absolute/path/to/samarth-gtm-mcp/dist/index.js"],
"env": {
"GTM_MCP_ENABLE_WRITES": "true"
}
}
}
}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 checkGET /oauth/callback— OAuth redirect handler
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 instanceSet 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 buildStart command:
GTM_MCP_TRANSPORT=http node dist/index.jsAdd 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 deployFly.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: If exposed publicly, add authentication middleware (API key header check, IP allowlist, or OAuth proxy). The current HTTP server has no built-in authentication beyond Google token validation for GTM calls.
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 inspectorProject 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.mdReleases
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 |
| Patch release ( |
|
| Minor release ( |
|
| Patch release |
|
| No release |
|
| Major release ( | 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-ciManual 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 fileOr set
GOOGLE_REFRESH_TOKENdirectly in.envif you prefer env-managed tokensIf 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.envRestart the server / Claude Desktop
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
Claude Desktop: MCP server not appearing
Check
claude_desktop_config.jsonfor JSON syntax errorsEnsure the path in
argsis an absolute path todist/index.jsMake sure
npm run buildhas been runRestart Claude Desktop completely (not just refresh)
TypeScript errors on googleapis types
Run
npm installto ensure all deps are installedThe
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.HTTP transport has no built-in user auth: the
/mcpendpoint must be fronted by your own auth layer (API key, IP allowlist, SSO proxy) for team/cloud deployments — 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: 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.
Built for Samarth Analytics by TagDrishti — Swapnil Jaykar
Maintenance
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/samarthanalytics-sj/samarth-analytics-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server