← Back to Blog
AI WorkflowAutomationAIAI Marketing

Wiring GA4, Search Console and a CRM to MCP

Model Context Protocol lets a model query your analytics stack directly. What that actually buys you, a real tool definition, and the three places it breaks.

SPSantosh Paudel· September 6, 2026· 14 min read
Table of contents

Model Context Protocol is a wire format for handing a language model tools it can call and data it can read. A server declares what it offers over JSON-RPC 2.0 — tools (functions the model executes), resources (context and data), prompts (templated workflows) — and any MCP-speaking host can connect to it. Wire GA4, Search Console and your CRM to one and the model stops guessing about your business and starts querying it. The current revision is 2026-07-28; its base protocol is stateless, with self-contained requests and per-request capability negotiation.

That is the whole idea. The rest of this post is the parts that bite.

Fair warning on evidence: I have no search-demand data for this topic. Nobody is finding my site for "MCP marketing stack" — I checked. What I do have is a Search Console property with numbers ugly enough to be useful, and an agent system in this repo whose tool surface I designed under the same constraints. Every claim below is either a linked primary source or my own data. The tool definition further down is a design written against the API documentation, not an extract from a server I run.

What the protocol actually specifies

The spec names three roles: hosts (the LLM application), clients (the connector inside it), servers (the thing exposing your data). Servers offer tools, resources and prompts. Clients can offer elicitation — the server asking the user a question mid-call.

The stateless turn

Older revisions leaned on a long-lived session. 2026-07-28 does not. The spec is explicit that there is no protocol-level session. Its advice for what to do instead — a server needing continuity across calls returns an opaque handle from a creation tool and takes it back as an argument — sits in a section the spec labels non-normative tool-design guidance, so treat it as the recommended pattern rather than a rule. For analytics work this barely matters, because every question is a fresh read. It matters enormously for anything that mutates state.

What "server" means in practice

A remote MCP server is an HTTP endpoint. That is the point of the stateless rework — it deploys like any other API you already run. My site runs on Vercel and Supabase, so exposing my own data over MCP would be a route handler with a service-role key and a column whitelist rather than a new piece of infrastructure to operate.

What wiring the three of them actually buys you

The interesting queries are the cross-source ones, because those are the ones nobody builds a dashboard for.

My own property, 85 days to 2026-09-03: 22 clicks, 4,553 impressions, 0.48% CTR, spread across 182 pages that drew at least one impression. Index coverage is a different report and a different number — roughly 390 URLs live. Russia contributed 1,320 impressions and zero clicks. The United States contributed 970 impressions at average position 38.8 and zero clicks. Nepal contributed 8 clicks from 74 impressions — 10.8% CTR at position 9.2.

No GA4 report answers "which pages pull impressions from countries that never convert". No CRM report answers "which of my closed deals first touched a page that ranks below position 30". Both are one join away if a model can read both. That is the buy: an end to the export-and-VLOOKUP tax.

The second thing it buys is honesty about what your content is worth, which is arithmetic you can also do by hand — my content ROI calculator runs the same math without any of the plumbing below, and is the faster answer if you are only asking the question once.

The official servers, and the gap

Google ships an official GA4 MCP server, built by the Analytics team. It exposes get_account_summaries, get_property_details, list_google_ads_links, run_report, run_realtime_report, run_funnel_report and get_custom_dimensions_and_metrics, authenticates through Application Default Credentials, and requires the analytics.readonly scope. Google states it plainly: the server is available for read requests only and cannot edit your configuration.

Search Console has no equivalent. I searched in September 2026 and found only community wrappers around the Search Console API — no server published by Google. If you want GSC in your stack you are either running someone else's code against your verified properties, or writing your own. I have not built one yet, and I will not be pointing a community wrapper at my properties — the same reason I would not paste a Supabase service key into a stranger's binary. Where API keys live and who can reach them is the boring prerequisite here.

A tool definition, concretely

This is the shape the spec defines. Name, optional title, description, JSON Schema input, optional JSON Schema output, optional annotations.

// gsc_search_analytics — MCP tool definition, 2026-07-28 shape
export const gscSearchAnalytics = {
  name: "gsc_search_analytics",
  title: "Search Console query report",
  description:
    "Read Search Analytics rows for one verified property. Returns clicks, " +
    "impressions, CTR and average position. Rows EXCLUDE anonymized rare " +
    "queries and stop at rowLimit, so the sum of row clicks runs BELOW the " +
    "property total whenever either applies. Never present a summed column " +
    "as the site total. Data is final after ~3 days; " +
    "dates are Pacific Time.",
  inputSchema: {
    type: "object",
    properties: {
      siteUrl: { type: "string", description: "e.g. sc-domain:example.com" },
      startDate: { type: "string", format: "date" },
      endDate: { type: "string", format: "date" },
      dimensions: {
        type: "array",
        items: { type: "string", enum: ["query", "page", "country", "device", "date"] },
        description:
          "Including 'page' switches aggregation from property to page level, " +
          "which changes the totals.",
      },
      rowLimit: { type: "integer", minimum: 1, maximum: 25000, default: 1000 },
    },
    required: ["siteUrl", "startDate", "endDate"],
    additionalProperties: false,
  },
  outputSchema: {
    type: "object",
    properties: {
      rows: { type: "array", items: { type: "object" } },
      rowCount: { type: "integer" },
      truncated: { type: "boolean", description: "true when rowCount === rowLimit" },
      aggregationType: { type: "string", enum: ["byProperty", "byPage"] },
    },
    required: ["rows", "rowCount", "truncated", "aggregationType"],
  },
  annotations: { readOnlyHint: true, openWorldHint: true },
} as const;

The description field is the whole product

Everything a model knows about your data before it calls you lives in that string. Every clause in the description above is a caveat the Search Console documentation buries three clicks deep, written out where the model will actually read it.

truncated is not part of any Google API response. The server has to compute it, because a model handed 1,000 rows with no signal that a 1,001st existed will summarise as if it saw everything. Returning the fact of truncation is cheaper than correcting the summary.

One caution straight from the spec: clients must consider tool annotations untrusted unless they come from a trusted server. readOnlyHint: true is a claim the server makes about itself. It is not a permission boundary. The boundary is the OAuth scope.

Where it breaks: auth and rate limits

Three scopes, and one of them is not really a scope

Three scopes exist in this stack and they are not interchangeable.

GA4 read-only is a clean OAuth scope Google publishes and the official server enforces. Search Console read is also clean. Your CRM is the problem. Most CRMs issue one API key carrying the permissions of the human who created it, which makes "let the model read pipeline stage counts" and "let the model read every contact email address" the same grant.

If your CRM cannot scope below the account level, the MCP server has to do the scoping: expose a database view rather than a table, and select only the columns you would be comfortable seeing pasted into a chat log.

The quotas you will actually hit

These are the published numbers. On a standard property row 1 and row 4 are the ones that bind; row 2 is here so you can see what the upgrade actually buys.

SurfaceLimitNotes
GA4 Data API, standard property40,000 tokens/property/hour; 200,000/day; 10 concurrent requestsEach request also spends from a separate 14,000 tokens/project/property/hour bucket
GA4 Data API, Analytics 3602,000,000 tokens/property/day; 50 concurrent10x the standard token quotas; concurrency is 5x (10 to 50)
GSC Search Analytics1,200 QPM per site and per userShort-term quota measured in 10-minute chunks
GSC URL Inspection600 QPM and 2,000 queries/day per siteThe daily cap is the one that bites
GSC row caprowLimit default 1,000, maximum 25,000Per query, not per day

Sources: GA4 Data API quotas and Search Console API limits.

The agent failure mode is specific. A model asked a vague question will decompose it into many small reports rather than one wide one, because small reports are easier to reason about. Thirty dimension-by-dimension GA4 calls will burn hourly token quota that one properly specified report would not. Cap concurrency in the server, cache identical requests, and put the daily URL Inspection budget in the tool description so the model can see it before it plans.

Where it breaks: the model will confidently misread your data

This is the failure that costs you money, because it is invisible.

Search Console removes rare queries from the table for privacy while keeping them in the chart total. It aggregates by property for query, country, device and date, and by page for pages and search appearance — add a page filter and the totals change. Data is usually available in 2 to 3 days. Dates are Pacific Time. Each of those is a documented, correct behaviour, and each of them produces a number that does not match the number next to it.

A model handed the rows and asked "what were my top queries" will sum the column and report a total. That total is short by every anonymized query and every row past the limit. On a property with no rare queries and a table under the cap the sum does match, which is the trap: nothing in the response tells you which case you are in.

# reconcile_gsc.py — the gap between a summed query table and the property total
QUERY_ROW_LIMIT_DEFAULT = 1000    # Search Analytics API default
QUERY_ROW_LIMIT_MAX = 25_000      # documented maximum

def unattributed(total_clicks: int, row_clicks: list[int]) -> tuple[int, float]:
    """Clicks the property total has that the named-query rows do not."""
    attributed = sum(row_clicks)
    gap = total_clicks - attributed
    return gap, (gap / total_clicks if total_clicks else 0.0)

if __name__ == "__main__":
    # Real: 22 total clicks on santoshpaudel.me, 85 days to 2026-09-03.
    # Worked example: an illustrative row breakdown, not my actual query table.
    gap, share = unattributed(22, [8, 3, 2, 1, 1, 1])
    assert gap == 6, gap
    print(f"{gap} clicks ({share:.0%}) sit outside the named-query rows")

    # Worked example: a site whose table hits the default limit.
    big = [5] * QUERY_ROW_LIMIT_DEFAULT
    gap2, share2 = unattributed(12_000, big)
    assert gap2 == 7_000, gap2
    assert round(share2, 4) == 0.5833, share2
    print(f"default rowLimit hides {share2:.0%} of clicks")

Both asserts pass. On my own site the row cap does not bite — only 182 pages ranked at all last quarter, and 111 of them earned five or fewer impressions. Anonymization does bite, at 22 total clicks. On a site with real traffic, the default limit alone can hide most of the clicks, and nothing in the API response says so.

The fix lives in the server, not the prompt

Telling the model to be careful does not work. What works: return truncated, return aggregationType, and return the property-level total alongside the rows so the discrepancy is visible in the same payload rather than inferrable from its absence. I cover the metric side of this in the five SEO metrics that actually matter and the interpretation side in marketing analytics for non-data people.

Tool results are untrusted input

The spec tells clients to validate tool results before passing them to the LLM, and to show tool inputs to the user before calling the server. Palo Alto Networks' Unit 42 documented attack vectors through MCP sampling in December 2025, including conversation hijacking and covert tool invocation by a malicious server. A CRM note field is user-submitted text. If your CRM tool returns note bodies verbatim, anyone who can fill in your contact form can write instructions into your agent's context.

What to expose read-only, and what never

SurfaceRead-only is fineNever expose
GA4run_report, run_realtime_report, property metadataAdmin API writes: custom dimension creation, data stream config, user links
Search ConsoleSearch Analytics query, sitemap list, URL Inspectionsitemaps.delete, sites.delete, indexing submission
CRMStage counts, deal ages, aggregate pipeline value, one workspace onlyContact emails in bulk, raw note bodies, anything that sends or schedules
Your own databaseA view with explicit columnsWrite access of any kind

That last row is a position I hold in code, not just in prose. Every create_* tool in my agent runner inserts into a pending_actions table for human approval, and no tool function contains an insert into a business table. Note where that control lives: the runner holds a service-role key that could write anywhere, so the boundary is the tool surface, and the way to remove a capability is to delete the function rather than tighten the key. The one honest exception is write_agent_memory, which upserts agent_memory directly — the agents' own scratchpad. The reasoning is in why my AI agents get an approval queue instead of write access, and the architecture in the autonomous marketing agent build.

Is it worth building

I have not shipped one. The part that has already paid, before any server of mine exists, is the read side and the discipline of writing the descriptions. Forcing myself to write down "the sum of these rows is not the total" in a tool description was the most useful hour I spent, because it made me admit which of my own numbers I had been reading wrong. That is a strange thing for a protocol to be good for, and it is the main thing I would sell it on.

FAQ

Is there an official Google Search Console MCP server?

No. As of September 2026 Google publishes an official GA4 MCP server but not a Search Console one. The available options are community wrappers around the Search Console API, or one you write yourself against the Search Console API directly.

Can an MCP server change my GA4 settings?

Google's official server cannot — it is read-only and requires the analytics.readonly scope. A third-party server can do whatever its OAuth grant allows, so check the scopes you are consenting to rather than the server's own description of itself.

Why do my MCP Search Console numbers not match the Search Console UI?

Anonymized queries are dropped from tables but kept in chart totals, aggregation switches between property and page level depending on your dimensions, data takes 2 to 3 days to finalize, and dates are Pacific Time by default. Any of the four will move a total.

How many API calls will an agent burn on one question?

More than you expect, because models decompose broad questions into many narrow reports. Cap concurrency server-side, cache identical requests, and state the remaining daily budget in the tool description.

Want your analytics stack readable by a model without handing it write access? I build these read-only, with the discrepancies surfaced in the payload rather than left for the model to guess. Run the content ROI numbers first or get in touch.

Free resource

Get the AI Automation Playbook

The real architecture behind a 6-agent AI content team — what it saves, what it gets wrong, and the propose-then-approve pattern that makes it safe to trust.

No spam. Unsubscribe anytime.

Browse all free guides →

Want to implement this with guidance?

Santosh helps founders turn insights like this into real systems.

AI Content Systems

External Resources

Further Reading & Tools

Related Posts

01
13 min
AI MarketingAI
TodayAI & Marketing

What 15 Marketing Tasks Cost in LLM Tokens

I priced 15 named marketing jobs against the live rate cards for Claude, GPT and Gemini. The whole set runs once for between $0.07 and $3.32. Editing the output costs about $317. That ratio is the post.

Read article
02
15 min
AI AgentsAutomation
TodayAI & Marketing

Autonomous Marketing Agent Architecture in Practice

Most writing about agentic marketing is speculation. This is the architecture of a system that runs on my own site every weekday: the task registry, the tool layer, the approval queue, the provider dispatch, and what it costs.

Read article
03
13 min
AI AgentsAutomation
TodayAI & Marketing

Why My Marketing Agent Proposes Instead of Publishing

My autonomous agents cannot write to a single business table. Every action they want to take lands in a pending_actions queue and waits for me to click approve. Here is the schema, the state machine, and the honest cost.

Read article