Skip to main content

Conversion Uplift

How to Connect Claude to Google Tag Manager With a Self-Hosted MCP Server

How to Connect Claude to Google Tag Manager With a Self-Hosted MCP Server

If you look after tagging for more than one or two clients, you already know how long a Google Tag Manager container review takes. Open a tag. Check the trigger. Check the variable it reads. Repeat forty times, then do the same again in the server-side container to see whether the two agree. A morning gone, and all you have at the end is a note that says “looks fine”.

A Google Tag Manager MCP server changes that arithmetic. It gives Claude a read-only door into your GTM configuration, so the reconciliation work becomes a conversation instead of forty browser tabs. In this guide I walk through the exact setup we use at Conversion Uplift: a self-hosted Google Tag Manager MCP server running on Google Cloud Run, reading GTM as a service account with Read permission and nothing more.

The video below shows the whole thing end to end. Every command is written out underneath, along with the five places it went wrong for me on camera, so you can avoid them.

On this page

What a Google Tag Manager MCP server actually does

MCP stands for Model Context Protocol. It is a standard way of giving an AI assistant a set of tools it is allowed to use. Anthropic created it for Claude, and it has since been adopted across all other major AI platforms.

The GTM MCP server itself is a small piece of software. It speaks the Google Tag Manager API on one side and MCP on the other. Once Claude is connected to it, you can ask questions in plain English and Claude reads the live configuration to answer them:

  • “Document every tag in this container, with its firing triggers and parameters.”
  • “Compare the parameters being sent with GA events by the web container against the parameters being sent by the All GA4 Events tags in the server container and tell me where they differ.”
  • “Which tags have no trigger attached?”
  • “Show me every tag that fires on all pages.”

That last one could take twenty minutes or more, depending upon how large the container. It now takes about fifteen seconds whatever the container size.

The work this replaces is auditing and reconciliation. Opening dozens of tags, triggers and variables by hand to confirm that two containers agree is the kind of task that is important, slow, and easy to get wrong when you are on the fortieth tag of the afternoon. Volume reports will not catch a mismatch either. Two conversion tags can both fire 1:1 in Google Ads and still be sending different values.

Why I self-host instead of using a hosted connector

There are ready-made hosted GTM connectors, Stape’s being the best known, and they are much quicker to switch on than the setup in this guide. For a single account, or an occasional look inside one container, they are a reasonable choice. I would not talk anyone out of starting there.

We tested the hosted route and it did not hold up to the way we work. The sticking point is authentication.

Hosted connectors typically sign in with an interactive OAuth token tied to one person’s Google login. That token expires and needs re-consenting. If you dip into a single account now and then, that is a minor annoyance. Across the number of client containers we review, it became a real obstruction: the connection kept dropping out and demanding re-authorisation part way through an audit, which meant starting the reconciliation again.

A self-hosted Google Tag Manager MCP server signs in as a service account through Workload Identity. That is a non-interactive machine identity with no login session to expire, so it keeps working unattended no matter how many client accounts you point it at. We grant that one identity Read access in each client’s GTM, and we control the whole thing: uptime, permission level, rate limits. Nothing depends on a third party’s token lifecycle.

My rule of thumb: if you have one or two containers, use a hosted connector. If you have ten, host it yourself.

How the read-only boundary works

Three things keep this safe, and they are worth understanding before you build it.

The service account only has Read. The server reads GTM as the service account. Because that account is a Read user in GTM, Google itself rejects any attempt to write, publish or delete. This is the entire safety boundary, which is why it never gets raised.

A bearer key protects the server. Every request has to carry the correct Authorization header. The key is a long random string you generate yourself and store in Google Secret Manager.

Workload Identity means no key file on the server. On Cloud Run the server borrows the service account’s identity automatically, so there is no JSON credential sitting on disk waiting to leak.

You can add Edit, Approve or Publish to that service account later. I would not add Publish. Publishing a container should stay a human decision made after someone has tested the change, not something an assistant can do because a prompt was worded loosely.

What you need before you start

You needWhy
A Google Cloud projectWhere the MCP server is hosted, on Cloud Run
Owner rights on that projectTo enable APIs, deploy and manage secrets
Billing enabledCloud Run and Cloud Build need a billing account attached. The free tier covers this comfortably
gcloud CLI, Git and Node.jsgcloud talks to Google Cloud, Git downloads the server, Node runs Claude Code
A service account with GTM ReadThe machine identity the server reads GTM as
A bearer keyThe password protecting your server
Claude CodeThe client that connects to the server

It is a one-time setup. Allow an hour the first time, then about ten minutes for each additional GTM account you share with the service account.

All the commands below are PowerShell on Windows. Set these three variables first and the rest of the guide copies straight across:

				
					$PROJECT = 'your-project-id'
$SA      = "mcp-server@$PROJECT.iam.gserviceaccount.com"
$REGION  = 'europe-west2'
				
			

Step 1: Create the Google Cloud project

Do this part in a browser, because the command line tools are not installed yet.

  1. Sign in at console.cloud.google.com with the Google account that has access to your GTM containers.
  2. Open the project picker in the blue bar at the top and choose New project.
  3. Give it a name. Google derives a project ID from that name, and the ID has to be unique across the whole of Google Cloud, so you may be offered something with digits appended. Write the ID down. Every command from here uses the project ID, not the name.
  4. Leave Location as No organisation unless your Workspace admin says otherwise, and click Create.

Then check two things in the Console. Under IAM and Admin, confirm your own account shows roles/owner. Under Billing, confirm a billing account is linked. If either is missing you will hit a wall four steps later, and the error message will not tell you that permissions were the cause.

Step 2: Install the tools

The most reliable route on Windows is the official installers:

Run each one and accept the defaults. On the Google Cloud CLI installer, choose Single user and keep Bundled Python ticked. Bundled Python is what saves you from version conflicts with any other Python on the machine.

Then close PowerShell, open a new window, and check all three:

				
					gcloud version
git --version
node -v
npm -v
				
			

Windows also has a package manager called winget, which installs all three in three lines:

				
					winget install --id Google.CloudSDK -e
winget install --id Git.Git -e
winget install --id OpenJS.NodeJS.LTS -e
				
			

Winget is optional and it needs Windows 10 version 1809 or later. If winget --version returns “not recognized”, go with the installers rather than spending twenty minutes on it. I lost that twenty minutes so you do not have to. More on that below.

Step 3: Sign in with gcloud

				
					gcloud init
				
			

Four prompts follow. Log in when asked, pick your Google account in the browser and approve the consent screen. When it asks which project to use, give it your project ID. When it offers to set a default Compute Region and Zone, answer n: that setting only affects Compute Engine, and Cloud Run takes its region explicitly in the deploy command.

Then confirm you are pointed at the right place and hold the rights you need:

				
					gcloud config set project $PROJECT
gcloud auth list
gcloud billing projects describe $PROJECT
				
			

You want a * beside your email address and billingEnabled: true.

Step 4: Create the service account and grant GTM Read

				
					gcloud iam service-accounts create mcp-server --display-name="GTM MCP (read-only)"
gcloud iam service-accounts list
				
			

The address follows the pattern [email protected]. Copy it exactly.

Now grant it access inside Google Tag Manager, once per GTM account you want Claude to read:

  1. Go to tagmanager.google.com and open the account.
  2. Admin, then User Management under the Account column.
  3. Click +, then Add users.
  4. Paste the service account address and set Account Permissions to Read, with Read on the containers too.
  5. Click Invite.

It takes effect straight away. A service account has no inbox, so there is no invitation for it to accept.

Step 5: Generate the bearer key and store it

Generate a strong random key:

				
					$bytes = New-Object 'System.Byte[]' 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$KEY = 'gtmk_' + ([Convert]::ToBase64String($bytes) -replace '\+','-' -replace '/','_' -replace '=','')
$KEY | Set-Clipboard
				
			

That copies the key to your clipboard without printing it on screen, which matters if you are recording or sharing a screen. Paste it into your password manager now. The gtmk_ prefix is a readable label, nothing more.

Enable the five APIs the deployment needs:

				
					gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com tagmanager.googleapis.com
				
			

Store the key as a secret. The file-writing step is not fussiness: piping a string to gcloud on Windows can add an invisible byte order mark or newline, and the server compares the key character for character. One stray character and every request comes back 401.

				
					$tmp = Join-Path $env:TEMP 'gtm-mcp-api-key.txt'
[System.IO.File]::WriteAllText($tmp, $KEY, (New-Object System.Text.UTF8Encoding($false)))
gcloud secrets create gtm-mcp-api-key --replication-policy="automatic" --data-file="$tmp"
				
			

Verify the stored copy matches, then tidy up and let the service account read it:

				
					$stored = gcloud secrets versions access latest --secret=gtm-mcp-api-key
$stored -eq $KEY
Remove-Item $tmp -Force
gcloud secrets add-iam-policy-binding gtm-mcp-api-key --member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"
				
			

$stored -eq $KEY must print True. That comparison is the reliable check. Do not try to eyeball the two strings.

Step 6: Deploy the Google Tag Manager MCP server to Cloud Run

Download the GTM MCP server code and confirm you are in the right folder:

				
					cd $env:USERPROFILE
git clone https://github.com/paolobietolini/gtm-mcp-server.git
cd gtm-mcp-server
Test-Path main.go, Dockerfile
				
			

Both checks should return True. Then deploy:

				
					gcloud run deploy gtm-mcp --source . --region $REGION --service-account $SA --set-secrets SERVICE_ACCOUNT_API_KEY=gtm-mcp-api-key:latest --allow-unauthenticated
				
			

Answer Y to the prompt about creating an Artifact Registry repository. The build takes a few minutes. On success it prints a Service URL like https://gtm-mcp-000000000000.europe-west2.run.app. Copy it.

--allow-unauthenticated sounds alarming and is correct here. The server does its own bearer key check, and switching on Cloud Run’s separate identity layer would clash with it. Security comes from the bearer key plus the Read-only service account.

Test the GTM MCP server before involving Claude:

				
					$URL = 'https://your-service-url.run.app'
curl.exe "$URL/health"
				
			

You want {"service":"gtm-mcp-server","status":"healthy","version":"1.8.2"}. One response proves three things: the container started, it picked up the service account, and it could read the secret.

Step 7: Connect Claude Code

Update Claude Code first. Older Windows builds drop the Authorization header on tool calls, which produces the maddening failure where your curl tests pass but Claude gets 401s.

				
					npm install -g @anthropic-ai/claude-code@latest
claude mcp add --transport http gtm-mcp $URL --header "Authorization: Bearer $KEY" --scope user
claude mcp list
				
			

You want gtm-mcp: https://... (HTTP) - ✓ Connected. The --scope user flag makes the connection available from any folder rather than only the one you registered it in, which is what you want if you run audits from client folders.

Then start a session and test it:

				
					claude
				
			

Type these as messages, approving each tool call:

				
					Use the gtm-mcp server's ping tool and show me the raw result.
				
			
				
					Use the gtm-mcp server to list the GTM accounts I have access to.
				
			

A pong response proves the chain from Claude Code through the bearer key to the Google Tag Manager MCP server on Cloud Run. Your GTM accounts appearing proves the service account permission works. At that point you are done.

Step 8: Use it from Claude Desktop as well

Claude Code is the right client for audit work, because it holds a long reconciliation in one go and can save what it finds to a file. Claude Desktop suits quick questions, and anyone on the team who would rather not open a terminal. Both talk to the same Google Tag Manager MCP server, and the read-only boundary is identical, because it comes from the service account rather than the client.

Desktop needs the config file rather than the connector dialog. Its Add custom connector screen offers an OAuth client ID and secret, with no field for an Authorization header, so it cannot authenticate against a bearer-key server on its own. The way round it is mcp-remote, a small bridge that runs locally and adds the header for you.

Open Claude Desktop, then Settings, Developer, Edit Config. On Windows the file sits at %APPDATA%\Claude\claude_desktop_config.json. Add this inside the mcpServers object, alongside anything already there, and mind the comma after the previous entry:

				
					"gtm-mcp": {
  "command": "npx",
  "args": [
    "-y",
    "mcp-remote",
    "https://your-service-url.run.app",
    "--header",
    "Authorization:${AUTH_HEADER}"
  ],
  "env": {
    "AUTH_HEADER": "Bearer your-key-here"
  }
}
				
			

Two details decide whether this works. The header has no space after the colon, because a space inside a single argument gets split on some platforms and the header then arrives malformed. The space belongs inside the environment variable, in front of the key. And if Claude Desktop reports that it cannot find npx, change "command" to "cmd" and put "/c", "npx" at the front of the args array, which is the usual fix on Windows.

Then quit Claude Desktop completely, including from the system tray, and reopen it. Closing the window does not reload the config. Test it with the same two prompts from Step 7: ask for the ping tool, then ask it to list your GTM accounts.

Worth knowing that your bearer key now sits in plain text in two files, Claude Desktop’s config and Claude Code’s .claude.json. That is normal for MCP clients. It matters when you rotate the key, because you change it in Secret Manager, redeploy, and then update both files.

Five things that tripped me up on camera

I recorded this setup live rather than editing out the friction, because the friction is where people give up.

Winget is not where you expect it. winget --version returned “not recognized” on a machine that had App Installer 1.29 installed and healthy. The package was there. The app execution alias was switched off, so PowerShell could not see it. Get-AppxPackage Microsoft.DesktopAppInstaller tells you whether it exists; Settings, Apps, Advanced app settings, App execution aliases turns the alias back on. Given that winget is only a shortcut for installing three tools, the installers are the faster answer.

Check before you install. The Google Cloud CLI installer stopped with “there already is a version installed”, because an earlier winget command had succeeded. The right response is No, then Cancel, then gcloud version. Installing over the top can leave two component manifests that disagree, which surfaces later as odd gcloud components errors.

PowerShell does not submit the last line of a pasted block. Paste the next command straight away and it gets appended to the unsubmitted line. My screen filled with A parameter cannot be found that matches parameter name 'Forcegcloud', which is two commands welded together. Press Enter once after each paste.

Windows PowerShell mangles inline JSON. This one is a genuine error in the first version of my own written guide. Sending the MCP handshake with curl.exe -d '{"jsonrpc":"2.0",...}' fails, because PowerShell 5.1 strips the double quotes before handing the string to curl. The server replies malformed payload: unmarshaling jsonrpc message, which reads like a server fault and is nothing of the kind. Write the body to a file instead:

				
					$body = '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
[System.IO.File]::WriteAllText("$env:TEMP\mcp-test.json", $body, (New-Object System.Text.UTF8Encoding($false)))
curl.exe -s -X POST "$URL/" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d "@$env:TEMP\mcp-test.json"
				
			

The error you get back is the pass. A reply saying "tools/list" is invalid during session initialization means your bearer key cleared the security layer and the server is speaking MCP, waiting for the handshake that Claude Code performs automatically. A flat 401 Unauthorized is the failure. I have watched people rebuild a working server because they read the first message as a fault.

Deploying outside the UK

The commands above deploy to europe-west2, which is London. If you are somewhere else, the region is the only thing that changes. Everything up to this point is global.

				
					$REGION = 'australia-southeast1'   # Sydney
				
			

Then deploy as written. Common choices are us-central1 (Iowa), us-east1 (South Carolina), europe-west1 (Belgium), asia-southeast1 (Singapore), asia-northeast1 (Tokyo), australia-southeast1 (Sydney) and southamerica-east1 (São Paulo). For the full list, run gcloud run regions list.

Pick the region nearest you, not nearest your client. The Tag Manager API is a single global Google endpoint, so matching a client’s country buys nothing. What you are shortening is the round trip between your own machine and Cloud Run, and that is what you feel while Claude works through a container.

Two things follow from changing region. The service URL contains the region, so a server deployed elsewhere gets a new hostname: if you have already registered it, run claude mcp remove gtm-mcp and add it again with the new URL. And the cloud-run-source-deploy Artifact Registry repository is created per region, so the first-run prompt about creating it appears again.

If data residency is the reason for the move, pin the bearer key to one region rather than Google-managed replication, and set a regional log bucket in Cloud Logging, because request logs default to global:

				
					gcloud secrets create gtm-mcp-api-key --replication-policy="user-managed" --locations="australia-southeast1" --data-file="$tmp"
				
			

Be straight with the client about what that covers. A regional deployment controls where your server and its logs live, and the server holds no GTM data at rest. It does not change where Google stores the container, because the API call goes to Google’s global endpoint either way. If a client needs that second guarantee, that is a question for Google rather than a deployment flag.

What it changes day to day

The honest answer is that the Google Tag Manager MCP server does not find things a careful analyst would miss. It finds them in ninety seconds instead of ninety minutes, and it does not get bored on the fortieth tag.

Where it earns its keep for us is web-to-server reconciliation. Comparing a client-side container against a server-side container to confirm that conversion IDs, labels, trigger conditions and consent settings line up is exactly where mismatches hide. Two containers can look correct in isolation and still disagree with each other.

What it does not do is verify that any of it is right. Claude reads the configuration you have. It cannot tell you that the purchase event is firing on a page it should not, that consent is being enforced in the wrong order, or that your revenue figure is 12% light because a tag fires before the dataLayer is ready. That still needs someone loading the site, walking the checkout and watching the network traffic. The MCP server tells you what the container says. A person tells you whether that matches reality.

That is the division of labour I would hold onto as more of this work gets automated. The machine reads faster than you can. Someone still has to be accountable for the answer.

Should you build this?

Build your own Google Tag Manager MCP server if you manage tagging across several GTM accounts, you are comfortable in PowerShell, and you want the connection to keep working without anyone re-authorising it. The hour it takes pays back on the second audit.

Use a hosted connector if you have one or two containers and want to try the idea this afternoon.

If what you actually want is confidence that your tracking is telling you the truth, the tooling is not the answer on its own. We run a free website analytics and SEO audit that shows what your setup is currently reporting and where it disagrees with itself. Neal Cole, who wrote the official GA4 and GTM audit course for CXL, reviews the findings. If you would rather have this built for you, we do that too.

Book Your Free Audit

Related reading: how to connect Claude to Google Analytics 4, common Google Tag Manager mistakes and the Google Analytics audit checklist.

Frequently asked questions

Is the Google Tag Manager MCP server read-only?

A Google Tag Manager MCP server is read-only if you configure it the way described here. The server reads GTM as a service account, and because that account holds Read permission in GTM, Google rejects any write, publish or delete. The permission level is the boundary, not the prompt wording.

What does it cost to run on Google Cloud?

Cloud Run bills per request and scales to zero when idle, so a container used for audits sits inside the free tier in normal use. You still need a billing account attached to the project, because Cloud Run and Cloud Build require one.

Do I need one MCP server for each client?

No. One self-hosted Google Tag Manager MCP server can read every GTM account that has shared Read access with your service account. Add the service account as a Read user in each client’s GTM and the same server covers them all.

Can I use this with Claude Desktop instead of the terminal?

Yes. Claude Desktop connects to the same server, though it needs the mcp-remote entry in its config file rather than the built-in connector dialog, because that dialog has no field for a bearer token. The section above has the exact JSON.

Can I run the Google Tag Manager MCP server outside the UK?

Yes. Change the --region value in the Cloud Run deploy command to any Cloud Run region and everything else stays the same. The service URL changes with the region, so re-register the server in Claude Code afterwards.

How long does the setup take?

Building the Google Tag Manager MCP server takes around an hour the first time, most of which is Google Cloud project admin and the Cloud Run build. Adding another GTM account afterwards takes a couple of minutes.

Full video transcript available below. Commands in this guide are PowerShell on Windows and were tested on 18/08/2026 against gtm-mcp-server v1.8.2.

Neal Cole

Neal Cole

Neal is the founder of Conversion Uplift and the author of the official GA4/GTM audit course for CXL. It's the course much of the industry trains on. He's spent over 20 years in digital analytics, including senior roles in online gaming and financial services, working out where businesses' numbers go wrong and what it costs them. His work now focuses on server-side tracking, GA4 and BigQuery: getting the data right, and being the person who's accountable for it when a decision depends on it.
Neal Cole

Neal Cole

Neal is the founder of Conversion Uplift and the author of the official GA4/GTM audit course for CXL. It's the course much of the industry trains on. He's spent over 20 years in digital analytics, including senior roles in online gaming and financial services, working out where businesses' numbers go wrong and what it costs them. His work now focuses on server-side tracking, GA4 and BigQuery: getting the data right, and being the person who's accountable for it when a decision depends on it.
Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Free audit checklist to

Fix GA4 Tracking Like a Professional

Acceptance
Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Free audit checklist to

Fix GA4 Tracking Like a Professional

Acceptance