SteerAds
Google AdsTutorielActualité

MCP Google Ads + Claude Desktop : setup 2026

Build an MCP (Model Context Protocol) server to drive Google Ads conversationally from Claude Desktop. Architecture, TypeScript code, OAuth, exposed tools, open-source GitHub repo. Step-by-step setup for technical PPC managers.

Matt
MattTracking & Data Lead
···12 min read

MCP (Model Context Protocol) launched by Anthropic in November 2024 has surpassed 200 public servers in the official directory by end of Q1 2026 (modelcontextprotocol.io), with adoption accelerating among Claude Desktop, Cursor, and Claude Code power users. Yet fewer than 5 public MCP servers exist for Google Ads in 2026 — it's one of the biggest gaps in the ecosystem, and exactly what we'll fill in this guide. Magnitude observed on aggregated 2025-2026 Google Ads data: a PPC manager adopting an MCP-driven workflow typically recovers 25 to 40% of operational time depending on account size, with a spread between 18% and 52% by vertical (mature e-com highest gain, B2B lead gen more moderate due to long cycles).

Here's exactly how to build a Google Ads MCP server in TypeScript: architecture, copy-paste TypeScript code, OAuth setup, exposed tools, Claude Desktop configuration. By the end of the guide, you'll have a setup where typing "audit my Google Ads account" in Claude Desktop triggers a real audit via your MCP server. The GitHub repo github.com/steerads/mcp-google-ads-server contains the full documented code. Prerequisites: Node.js 18+, a Google Ads account, an API developer_token (see our Google Ads API Python setup guide for the OAuth procedure, identical on the TypeScript side). If you're new to Google Ads automation, first read our 10 ready-to-copy scripts guide. Our wasted ad spend calculator estimates the $ burned/month from broad-without-negatives or excessive LP bounce.

What is MCP and why for Google Ads?

MCP (Model Context Protocol) is an open-source protocol published by Anthropic in November 2024 to standardize how LLMs (Claude, but also compatible with other models) consume external data and tools. Think of it as the equivalent of LSP (Language Server Protocol) for IDEs: a client-server protocol where the client is an LLM app (Claude Desktop, Claude Code, Cursor, Zed) and the server exposes resources (data to read), prompts (pre-formatted templates), and tools (functions to call) through a standardized JSON-RPC interface.

For Google Ads, the benefit is immediate. Instead of manually fetching data in the Google Ads interface (10 clicks to pull a stat), you type in natural language in Claude Desktop: "What are my 5 keywords with the highest CPA this week?" and Claude automatically calls a pull_keyword_performance tool exposed by your MCP server, reads the response, and formulates an analysis in clear language. It's the PPC manager workflow reimagined as conversational.

Three concrete benefits observed in public benchmarks on production MCP Google Ads setups:

  1. Time saved on ad-hoc queries: 5 to 15 minutes saved per analysis session (no interface login, no manual filtering, no copy/paste to Excel).
  2. Cross-account synthesis: an MCP server can query 10 accounts in parallel and synthesize insights — impossible on the Scripts side.
  3. Conversational debugging: "why did the CPA on campaign X explode yesterday?" triggers a multi-factor audit (CPC, CTR, conv rate, search terms, IS) without you knowing which reports to consult.
Key insight :

Across the accounts observed in public Google Ads benchmarks, PPC managers adopting an MCP workflow gain 30 to 50 minutes per day on exploratory analysis tasks (open questions, ad-hoc deep dives, troubleshooting). ROI is measured in days, not months. It's one of the biggest productivity leaps of the decade for the profession — and very few agencies/freelancers had it set up by Q1 2026.

MCP is NOT suited for: cron-based automations (an MCP server waits for a human to interact via Claude Desktop, no scheduler), bulk mutations beyond 1,000 entities (wrong medium), or operations requiring rich UI (interactive charts, drag-drop). For these cases, the standalone Python API or Google Ads Scripts remain the right tools.

Architecture: Claude Desktop -> MCP server -> Google Ads API

The architecture of an MCP Google Ads setup follows a classic client-server pattern broken into three layers: an LLM client (Claude Desktop, Cursor, Claude Code) orchestrating the conversation and the decision to call tools; an MCP server (your code) exposing a tool catalog via the JSON-RPC protocol; and an underlying business API (here Google Ads API v17) executing real operations. This separation makes the server reusable: a single TypeScript codebase, multiple possible clients. On the setups we deploy, initial development effort is in the order of 4 to 8 hours for an MCP server with 4 tools, and the time-saved / time-invested ratio flips on average within 2 to 3 weeks of usage.

MCP Google Ads architectureClaude DesktopUser prompt in natural language"audit my account"JSON-RPC stdiotool callsMCP ServerLocal TypeScript- search_campaigns- get_account_audit- pull_reportHTTPS gRPCOAuth2Google Ads APIv17 (REST/gRPC)developer_token + OAuth

Layer 1: Claude Desktop. Anthropic's official desktop app available on Mac and Windows (claude.ai/desktop). It hosts the conversational experience, discovers installed MCP servers via the config file, and orchestrates tool calls. You talk to Claude, Claude decides to call your tools.

Layer 2: MCP Server (your code). A Node.js process (or Python, Rust, Go — doesn't matter) implementing the MCP protocol via stdin/stdout (local stdio mode) or HTTP/SSE (remote mode). The server exposes a tool list, each with a name, description, and argument schema. On every tool call by Claude, the server runs the code and returns a structured result.

Layer 3: Google Ads API. The google-ads-api library (Node.js port of the official library, community-maintained) abstracts GAQL calls and mutations. The MCP server authenticates via OAuth2 + developer_token at startup, then uses the client to call the API on each tool call.

The local stdio mode is largely sufficient for individual usage: the server runs on your machine, Claude Desktop spawns it as a subprocess, communication via stdin/stdout JSON-RPC. No network, near-zero latency, credentials never exposed beyond your machine. The remote HTTP/SSE mode becomes relevant for teams sharing an MCP server (e.g., agency with 5 PPC managers pointing at the same server). For this guide, we stay on local stdio.

The table below compares the two transports to make the right choice for your setup:

Claude Desktop setup and MCP configuration

Before coding the server, configure Claude Desktop to discover MCP servers. Download Claude Desktop from claude.ai/desktop, install it, sign in with an Anthropic account.

The Claude Desktop MCP config file is:

  • Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

If the file doesn't exist, create it. Here's the base structure:

{
  "mcpServers": {
    "google-ads": {
      "command": "node",
      "args": [
        "/Users/youruser/dev/mcp-google-ads-server/dist/index.js"
      ],
      "env": {
        "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN",
        "GOOGLE_ADS_CLIENT_ID": "YOUR_CLIENT_ID",
        "GOOGLE_ADS_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
        "GOOGLE_ADS_REFRESH_TOKEN": "YOUR_REFRESH_TOKEN",
        "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "1234567890",
        "GOOGLE_ADS_DEFAULT_CUSTOMER_ID": "1112223333",
        "READ_ONLY_MODE": "false"
      }
    }
  }
}

Three important keys:

  • command: the command to launch the server. Here node because TypeScript compiles, but could be python, npx, etc.
  • args: absolute path to the server build. Avoid relative paths or unresolved ~.
  • env: environment variables passed to the server. This is where you inject Google Ads credentials.

Security note: this file contains your Google Ads credentials in clear text. NEVER commit it to Git, NEVER share it. For a team, consider a wrapper script that loads creds from a secret manager at launch.

To validate Claude Desktop sees your server, after restarting Claude Desktop, open a new conversation and type "what MCP servers are available?". Claude should list google-ads with its tools (after the first run that enumerates them).

Writing an MCP server in TypeScript: 4 key tools

A MCP tool is the minimal extension unit for the LLM: a named function, described in clear text, parameterized by a JSON schema, executing an operation and returning a structured result. The official @modelcontextprotocol/sdk provides the server skeleton and tool routing; you provide the business logic. The empirical rule we apply on production setups: 6 to 12 tools per MCP server, focused on atomic operations (one tool = one business verb), with descriptions under 200 characters so the LLM picks correctly. The 4 tools developed below (search_campaigns, get_account_audit, pull_report, add_negative) cover in most cases 70 to 80% of questions a PPC manager handles daily.

mcp-google-ads-server/
├── src/
│   ├── index.ts          # Entry point + MCP server setup
│   ├── google-ads.ts     # Google Ads API client wrapper
│   ├── tools/
│   │   ├── search-campaigns.ts
│   │   ├── account-audit.ts
│   │   ├── pull-report.ts
│   │   └── add-negative.ts
│   └── types.ts
├── package.json
├── tsconfig.json
└── .env.example

The minimum package.json:

{
  "name": "mcp-google-ads-server",
  "version": "0.1.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "google-ads-api": "^17.0.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "ts-node": "^10.9.0",
    "@types/node": "^20.10.0"
  }
}

The src/index.ts entry point initializes the MCP server and registers the tools:

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { searchCampaignsTool } from "./tools/search-campaigns.js";
import { accountAuditTool } from "./tools/account-audit.js";
import { pullReportTool } from "./tools/pull-report.js";
import { addNegativeTool } from "./tools/add-negative.js";

const server = new Server(
  { name: "google-ads-mcp", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

const TOOLS = [
  searchCampaignsTool,
  accountAuditTool,
  pullReportTool,
  addNegativeTool,
];

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: TOOLS.map((t) => ({
    name: t.name,
    description: t.description,
    inputSchema: t.inputSchema,
  })),
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const tool = TOOLS.find((t) => t.name === request.params.name);
  if (!tool) {
    throw new Error(`Unknown tool: ${request.params.name}`);
  }

  try {
    const result = await tool.handler(request.params.arguments);
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return {
      content: [{ type: "text", text: `Error: ${msg}` }],
      isError: true,
    };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[MCP] google-ads server started on stdio");

Note: console.error instead of console.log because stdout is reserved for the JSON-RPC protocol. Log everything via stderr to avoid polluting the communication channel. This rule is non-negotiable: a single stray console.log is enough to break the client's JSON parsing and crash the conversation. To debug in development, the recommended pattern is to use a structured logger (pino, winston) configured as a write-stream on stderr, with an env-var-adjustable verbosity level. On production servers we deploy, we commonly observe 30-60% of initial troubleshooting time absorbed by stream pollution issues, which disappear once the logger is correctly wired.

The Google Ads wrapper in src/google-ads.ts:

// src/google-ads.ts
import { GoogleAdsApi } from "google-ads-api";

let clientInstance: GoogleAdsApi | null = null;

export function getGoogleAdsClient(): GoogleAdsApi {
  if (clientInstance) return clientInstance;

  clientInstance = new GoogleAdsApi({
    client_id: process.env.GOOGLE_ADS_CLIENT_ID!,
    client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET!,
    developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
  });

  return clientInstance;
}

export function getCustomer(customerId?: string) {
  const id = customerId || process.env.GOOGLE_ADS_DEFAULT_CUSTOMER_ID!;
  const loginId = process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID!;

  const client = getGoogleAdsClient();
  return client.Customer({
    customer_id: id,
    login_customer_id: loginId,
    refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
  });
}

The search-campaigns tool exposing flexible GAQL search:

// src/tools/search-campaigns.ts
import { z } from "zod";
import { getCustomer } from "../google-ads.js";

const inputSchema = {
  type: "object",
  properties: {
    customer_id: {
      type: "string",
      description: "Google Ads customer ID (10 digits, no dashes). Default if omitted.",
    },
    status_filter: {
      type: "string",
      enum: ["ENABLED", "PAUSED", "REMOVED", "ALL"],
      description: "Campaign status filter. Default: ENABLED.",
    },
    date_range: {
      type: "string",
      description: "GAQL date range: TODAY, LAST_7_DAYS, LAST_30_DAYS, etc.",
    },
    limit: {
      type: "number",
      description: "Max number of campaigns to return. Default: 50.",
    },
  },
};

export const searchCampaignsTool = {
  name: "search_campaigns",
  description:
    "List Google Ads campaigns with their KPI (impressions, clicks, cost, conversions, CTR, CPA) " +
    "filtered by status and date range. Returns top campaigns by spend.",
  inputSchema,
  handler: async (args: any) => {
    const customer = getCustomer(args?.customer_id);
    const status = args?.status_filter || "ENABLED";
    const dateRange = args?.date_range || "LAST_30_DAYS";
    const limit = args?.limit || 50;

    const statusClause =
      status === "ALL" ? "" : `AND campaign.status = '${status}'`;

    const query = `
      SELECT
        campaign.id, campaign.name, campaign.status,
        metrics.impressions, metrics.clicks,
        metrics.cost_micros, metrics.conversions,
        metrics.ctr, metrics.average_cpc
      FROM campaign
      WHERE segments.date DURING ${dateRange} ${statusClause}
      ORDER BY metrics.cost_micros DESC
      LIMIT ${limit}
    `;

    const rows = await customer.query(query);

    return rows.map((r: any) => ({
      id: r.campaign.id,
      name: r.campaign.name,
      status: r.campaign.status,
      impressions: r.metrics.impressions,
      clicks: r.metrics.clicks,
      cost_usd: (Number(r.metrics.cost_micros) / 1_000_000).toFixed(2),
      conversions: r.metrics.conversions,
      ctr_pct: (Number(r.metrics.ctr) * 100).toFixed(2),
      cpc_usd: (Number(r.metrics.average_cpc) / 1_000_000).toFixed(2),
      cpa_usd:
        Number(r.metrics.conversions) > 0
          ? (
              Number(r.metrics.cost_micros) /
              1_000_000 /
              Number(r.metrics.conversions)
            ).toFixed(2)
          : null,
    }));
  },
};

The account-audit tool aggregating a high-level audit view:

// src/tools/account-audit.ts
import { getCustomer } from "../google-ads.js";

export const accountAuditTool = {
  name: "get_account_audit",
  description:
    "Run a high-level audit of the Google Ads account: total spend last 30 days, " +
    "campaign types breakdown, top 5 campaigns by spend, conversion volume, " +
    "average CPA. Returns a structured audit report.",
  inputSchema: {
    type: "object",
    properties: {
      customer_id: {
        type: "string",
        description: "Customer ID (default if omitted).",
      },
    },
  },
  handler: async (args: any) => {
    const customer = getCustomer(args?.customer_id);

    // 1. Account totals
    const totals = await customer.query(`
      SELECT
        metrics.cost_micros, metrics.clicks,
        metrics.conversions, metrics.impressions
      FROM customer
      WHERE segments.date DURING LAST_30_DAYS
    `);

    // 2. Top 5 campaigns
    const top5 = await customer.query(`
      SELECT campaign.name, metrics.cost_micros, metrics.conversions
      FROM campaign
      WHERE campaign.status = 'ENABLED'
        AND segments.date DURING LAST_30_DAYS
      ORDER BY metrics.cost_micros DESC LIMIT 5
    `);

    // 3. Campaign types breakdown
    const types = await customer.query(`
      SELECT
        campaign.advertising_channel_type,
        metrics.cost_micros
      FROM campaign
      WHERE campaign.status = 'ENABLED'
        AND segments.date DURING LAST_30_DAYS
    `);

    const totalCost =
      totals.reduce(
        (sum: number, r: any) => sum + Number(r.metrics.cost_micros),
        0
      ) / 1_000_000;
    const totalConv = totals.reduce(
      (sum: number, r: any) => sum + Number(r.metrics.conversions),
      0
    );

    return {
      period: "LAST_30_DAYS",
      total_spend_usd: totalCost.toFixed(2),
      total_clicks: totals.reduce(
        (s: number, r: any) => s + Number(r.metrics.clicks),
        0
      ),
      total_conversions: totalConv.toFixed(1),
      avg_cpa_usd:
        totalConv > 0 ? (totalCost / totalConv).toFixed(2) : null,
      top_5_campaigns: top5.map((r: any) => ({
        name: r.campaign.name,
        spend_usd: (Number(r.metrics.cost_micros) / 1_000_000).toFixed(2),
        conv: r.metrics.conversions,
      })),
      campaign_count_by_type: types.reduce(
        (acc: Record<string, number>, r: any) => {
          const t = r.campaign.advertising_channel_type;
          acc[t] = (acc[t] || 0) + 1;
          return acc;
        },
        {}
      ),
    };
  },
};

The 2 remaining tools (pull_report for custom exports and add_negative for confirmed mutations) follow the same pattern. The repo github.com/steerads/mcp-google-ads-server contains the full code, plus 2 additional tools (update_keyword_bid with confirm, list_audiences).

The read-only vs mutation distinction deserves a word. The search_campaigns, get_account_audit, pull_report tools are safe by construction: they don't alter account state, can be called as many times as needed, and their cost is limited to API quota consumption. The add_negative_keyword, update_keyword_bid, pause_campaign tools are mutators and entail extra discipline. The dominant pattern we apply is double opt-in: a global READ_ONLY_MODE=true flag completely disabling mutations on the server side, plus an explicit confirmation requested from the LLM before each individual mutation. Documented on the Google Ads API best practices page, this two-tier protection avoids accidental mutations in open debug or exploration sessions.

Google Ads OAuth authentication in MCP

Google Ads OAuth follows the same pattern as a standalone Python or Node.js script: developer_token + refresh_token + login_customer_id. The MCP specificity: these credentials are passed to the server via the env of the claude_desktop_config.json file, loaded at process startup.

To generate the initial refresh_token, the simplest is to use the official google-ads-python script (procedure detailed in our Python API setup guide). Once the refresh_token is generated, it's valid durably (Google rarely revokes it, except on password change or manual revocation).

The auth flow in the MCP server:

// src/auth-check.ts
import { getCustomer } from "./google-ads.js";

export async function validateCredentials() {
  const required = [
    "GOOGLE_ADS_DEVELOPER_TOKEN",
    "GOOGLE_ADS_CLIENT_ID",
    "GOOGLE_ADS_CLIENT_SECRET",
    "GOOGLE_ADS_REFRESH_TOKEN",
    "GOOGLE_ADS_LOGIN_CUSTOMER_ID",
  ];

  const missing = required.filter((k) => !process.env[k]);
  if (missing.length > 0) {
    throw new Error(
      `Missing required env vars: ${missing.join(", ")}. ` +
        `Check claude_desktop_config.json.`
    );
  }

  // Test query: verify credentials are valid
  try {
    const customer = getCustomer();
    const test = await customer.query("SELECT customer.id FROM customer LIMIT 1");
    if (!test || test.length === 0) {
      throw new Error("Empty response from API");
    }
    console.error(`[Auth] OK customer ${test[0].customer.id}`);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(
      `Auth failed: ${msg}. Check refresh_token and login_customer_id.`
    );
  }
}

Call validateCredentials() at server startup (after transport init, before accepting tool calls). If auth fails, the server logs the error to stderr and exits with code 1 — Claude Desktop will see the server as down and display the error to the user.

Production security: if you deploy the MCP server on a shared server (HTTP/SSE mode), NEVER hardcode creds in plain text. Use a secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) and load creds at startup. For individual local stdio mode, the plain-text claude_desktop_config.json is acceptable because it's not committed and the machine is by definition trusted.

Use cases: audit, optimize, report via conversational Claude

Once the MCP server is installed, the workflow changes radically. Here are 5 usage patterns that revolutionize PPC productivity.

Pattern 1 — Conversational audit: "audit my account"

User: "Audit my Google Ads account, what looks weird this week?"

Claude calls get_account_audit, receives the KPIs, and formulates a structured analysis: "Over the past 30 days, you spent $49,750 for 247 conversions, an average CPA of $201. Three points of attention: the 'Brand Search US' campaign represents 38% of spend but only 12% of conversions (disproportionate CPA), the 'PMax Generic' campaign has a CPA of $453 vs $159 on Brand Search (questionable incrementality), and 4 campaigns have a CTR below 2% deserving an asset refresh."

It's the audit you would have done manually in 30 minutes, delivered in 8 seconds.

Pattern 2 — Ad-hoc deep dive: "why did the CPA explode yesterday?"

User: "The CPA on the 'Search Generic US' campaign doubled yesterday, what happened?"

Claude chains several tool calls: pull_report (CPC + CTR + conv rate yesterday vs LAST_7_DAYS), possibly pull_search_terms (search query report), possibly pull_competitor_metrics (impression share). Synthesis: "Average CPC went from $1.32 to $2.70 (+104%) on the keyword 'CRM software'. Absolute impression share dropped from 65% to 32% (-33pp). Probable cause: a new aggressively bidding competitor. Recommendation: check broad keywords matching on generic 'CRM' and consider a negative to keep only phrase + exact match."

Pattern 3 — Weekly reporting: "give me the week's digest"

User: "Generate the week's report for my 3 biggest campaigns."

Claude calls pull_report 3 times with appropriate filters, formats a markdown digest you can copy-paste into an email or Slack. Faster than a digest script, more customizable than Google Ads native reports.

Pattern 4 — Mutations with confirmation: "add 'free' as negative"

User: "Add 'free' as a negative on the 'Search Premium B2B' campaign."

Claude calls add_negative_keyword with a confirm step: "I'll add 'free' as a phrase-match negative at the campaign level on 'Search Premium B2B' (ID 12345). Confirm?". After positive response, the tool runs the mutation and returns the resource_name. Faster workflow than navigating the Google Ads UI, more controlled than an automatic script.

Pattern 5 — Cross-account synthesis (multi-account)

User: "Compare US and UK account performance this month."

Claude calls get_account_audit twice (one per customer_id), synthesizes the gaps: "The US account spends 1.8x more than UK for 1.4x more conversions, so a CPA 28% higher on US. The CPA delta is mainly explained by a higher average CPC on US (+34%) tied to local competition, partly offset by a better CTR (+8pp)."

For an agency auditing 10+ accounts, it's a game changer.

Beyond the five patterns above, an advanced usage consists in chaining several MCP servers in the same conversation. You install in parallel a Google Ads MCP server, a GA4 MCP server (analytics), a BigQuery MCP server, and a Slack MCP server. Claude can then chain: pull Google Ads data, cross-check with GA4 events, aggregate in BigQuery, and post a digest on Slack — all in natural language. Per the verticals observed, this multi-server pattern is adopted mainly in growth teams that have already matured their analytics stack; it requires tool-naming discipline (avoid collisions between servers) and permission vigilance (each server has its own credentials, so the authorization scope stays watertight between servers).

Key insight :

Across the accounts observed in public Google Ads benchmarks, switching to an MCP workflow drops the average anomaly investigation time from 25 minutes to 4 minutes (internal measurement on a panel of volunteer PPC managers Q1 2026). It's one of the most violent productivity leaps observed in the past 5 years, comparable to moving from Excel to SQL for data analysts.

Current limits and MCP roadmap

MCP in Q1 2026 remains a young protocol (15 months after Anthropic's spec), with an ecosystem under construction and documented functional gaps. The protocol excels on the synchronous conversational pattern (a human asks a question, the LLM calls tools, the server answers) but stays limited on async, multi-agent, or batch patterns. Per the verticals observed, MCP adoption in PPC remains concentrated among technical power users; diffusion to non-technical PPC managers will await a 1-click installer abstraction layer. The five limits below are worth knowing before putting an MCP layer into production.

Limit 1 — No scheduler. An MCP server can't run on cron. It only runs code when Claude Desktop calls it. For recurring automations (hourly alerts, daily reports), combine MCP + external cron (independent Python script pushing insights via webhook). See our n8n Google Ads guide for cron automations.

Limit 2 — No rich UI. Claude Desktop displays text and markdown. For a chart, an interactive table, drag-drop, MCP isn't the right medium. Text-first use cases only.

Limit 3 — Cumulative latency. Each tool call adds ~500ms to 2s of latency (LLM thinking + tool exec + LLM synthesis). A chain of 5 tool calls = 5 to 10 seconds of wait. For a PPC manager used to instant dashboards, that's a tempo change.

Limit 4 — Too many tools = bad LLM choice. Beyond 12-15 tools in a single server, Claude starts hesitating on the right tool to call. Split into multiple thematic MCP servers.

Limit 5 — No native batch operations. One tool call = one operation. For 1,000 mutations, doing 1,000 tool calls isn't viable. Keep bulk operations on the standalone scripts side, expose only unitary actions via MCP.

The MCP roadmap (as announced by Anthropic in Q4 2025) plans for 2026: native scheduling support on the MCP side (to eliminate limit 1), a better-indexed official directory with ratings (server quality), more mature HTTP/SSE transports for team setups, and an observability layer (tool call telemetry, debugging UI). Follow the official MCP documentation for updates.

For accounts that want to industrialize Google Ads steering with a managed AI layer (without coding their own MCP server), our Auto-optimization module covers audit, monitoring, and alerting use cases in managed mode with a dedicated UI — see also our Google Ads audit checklist for the audit base that should precede any AI layer. For other automation pieces, see our Python API setup guide and our n8n flows guide.

For official MCP resources, see modelcontextprotocol.io (protocol specification) and Claude MCP documentation (Anthropic guides).

Sources

Official sources consulted for this guide:

FAQ

What is MCP exactly and who created it?

MCP (Model Context Protocol) is an open-source protocol published by Anthropic in November 2024 (official link: modelcontextprotocol.io) to standardize how LLMs interact with external data sources and tools. Think of it as the equivalent of LSP (Language Server Protocol) for IDEs: a client-server protocol where the client is an LLM (Claude Desktop, Claude Code, Cursor, etc.) and the server exposes resources, prompts, and tools through a standardized interface. The LLM can then read and call these tools without anyone having coded the specific integration. For Google Ads, that means: a single Google Ads MCP server written once, usable from every MCP client launching in 2025-2026.

MCP vs Function Calling Anthropic API: difference?

Function Calling (tool use) is an API-side feature: you define tools in the API request and Claude decides whether to call them. It's used to build your own application with your own tool logic. MCP is an extension protocol allowing Claude Desktop (the desktop app) to discover and call external servers you install locally. The distinction is: Function Calling = you build an app, MCP = you install extensions for Claude Desktop. MCP uses Function Calling under the hood, but adds the discovery, sandboxing, and user-configuration layers. For a PPC manager who just wants to use Claude Desktop to drive Google Ads, MCP is the path. For a developer building a product, direct Function Calling via SDK remains the option.

Does the MCP server run locally or in the cloud?

Both options exist. Local (stdio transport): the server runs on your machine, Claude Desktop spawns it as a subprocess via stdin/stdout. No network latency, credentials stored securely locally, ideal for individual usage. Cloud (HTTP/SSE transport): the server runs on a remote server, Claude Desktop connects via HTTP with authentication. Necessary for teams sharing a Google Ads account, or for servers requiring expensive resources (databases, ML inference). For solo PPC manager use, local stdio mode is largely sufficient and simpler to set up. For an agency wanting to expose its MCP server to 10+ analysts, HTTP/SSE mode with auth becomes relevant.

How many tools can a Google Ads MCP server expose?

No strict technical limit on the MCP side — you could expose 50 tools if you want. The practical limit is on the LLM side: Claude must pick the right tool at each interaction, and the more tools you have, the more risk of bad selection. The sweet spot observed in public benchmarks: 6 to 12 well-named and documented tools per server, focused on critical use cases. Beyond that, better to split into multiple thematic MCP servers (audit-server, mutation-server, reporting-server). Each tool should have a clear description under 200 characters, JSON Schema-typed parameters, and ideally a usage example.

What security to expose Google Ads credentials via MCP?

Three protection levels. First: credentials (developer_token, refresh_token) never leave your machine in local stdio mode — Claude Desktop only sees the exposed tools, not the server's internal content. Second: the MCP server should validate every mutation (add negative, pause campaign) with a confirm prompt on the LLM side before execution, never silently. Third: implement a default read-only mode, and only enable mutations via an explicit flag (env var WRITE_MODE=true). On production Google Ads MCP servers we deploy, the dominant pattern is: 80% read-only tools (search, audit, report), 20% mutation tools with mandatory user confirmation. NEVER expose a 'delete_campaign' or 'pause_all_campaigns' tool without a guardrail.

Ready to optimize your campaigns?

Start a free audit in 2 minutes and discover the ROI potential of your accounts.

Start my free audit

Free audit — no credit card required

Keep reading