PerfoAds MCP

com.perfoadsPublished by perfoads.comv0.2.0-alpha.1Updated Nov 8, 2026

Checks your Google Ads accounts (162 checks) and changes them only after you approve. Self-hosted.

VerifiedSTDIODesktop onlyData & AnalyticsBusiness & Commerce

Overview

AI-generated overview

Self-hosted MCP server that lets an assistant read Google Ads accounts, run 162 account checks, and apply changes only after human approval.

What you can do
  • Check setup and list accessible Google Ads accounts, including those under manager accounts
  • Read account overview, campaigns, ad groups, keywords, search terms, budgets, negatives, ads, audiences, conversions and change history
  • Run read-only GAQL queries and inspect resource fields
  • Build a snapshot and run 162 deterministic checks, with findings, evidence and severity
  • Propose, approve, apply, undo and track changes to budgets, status, bids, negatives, keywords and ads
When to use it
Useful when you want an assistant to audit Google Ads accounts, explain what is wasting spend, and make limited, explicitly approved changes without leaving your own Google Cloud project.
Requirements
Local process run via npx (Node.js 22.13 or later). Needs your own Google Cloud project with the Google Ads API enabled, a Desktop-app OAuth client, and Explorer access approval; until approved, only test accounts work. Optional PERFOADS_MCP_HOME sets the settings folder. Network access to Google APIs.
Before you install
Stores OAuth client id, secret and refresh token encrypted in a local settings folder; any process running as your OS user can read the key, so use the hardened setup if the agent has a shell or browser control. Changes are off until an account is listed under accounts.write in policy.yaml; approval happens on a local page with a code, and an AI saying yes approves nothing. The auto tier can pause campaigns or lower budgets without per-change approval. Early alpha.

Installation

In SourceWeft

  1. Open PerfoAds MCP in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

[PerfoAds MCP]

PerfoAds MCP

A self-hosted MCP server that works with Google Ads. It lets an AI assistant (Claude Desktop, Claude Code, Cursor and other MCP apps) read your Google Ads accounts, run 162 deterministic account checks, and change things only with your approval and within limits you set in a policy file.

PerfoAds MCP works with Google Ads. It is not made, endorsed or supported by Google (not a Google product). It works with the Google Ads API through your own Google Cloud project and your own sign-in; nothing goes through anyone else's servers.

Package and CLI: perfoads-mcp. License: Apache-2.0. Status: early alpha. On npm: perfoads-mcp (0.2.0-alpha.1).

Work with Paweł

The checks say what is wrong: the evidence, how serious it is and the money it wastes. They do not say how to fix it. Want fixes, not just findings? PerfoAds adds fix plans; Paweł Wilk offers a free 1-hour call - perfoads.com

The safety promise

No change can be applied through MCP without a human approving it outside the model. On hosts where the agent also has a shell or controls the browser (Claude Code, Cursor, browser-control tools) this holds only with the hardened setup (server + store under a separate OS user or in a container, approval_mode: code_only and open_browser: false), and only if you do the human steps (your password, approving, policy edits) from a session the AI can't change: another account's login session, SSH from another device, or with the AI agent sandboxed. An AI with a shell on your user can change your shell startup files, fake sudo and capture what you type.

The one exception is one you make yourself: the auto tier in your policy file (off unless you set it, and only for pausing a campaign or lowering a budget).

What this means in practice:

  • Changes are off until your policy file lists an account under accounts.write.
  • Google checks the change first without applying it (validate only). Then it is shown to you on a local approval page with the account name, id and currency first, the exact change, every campaign it reaches, and the AI's reason in a separate "unverified" box.
  • You approve with the 6-character code from that page (typed into your AI app's form or a terminal), or a click on the page in apps without forms. An AI saying "yes" approves nothing.
  • The server applies the change once, reads it back from Google, and logs every step in a hash-chained audit log. Most changes can be undone within 30 days (as a new approved change); see the per-type table.

Read docs/THREAT-MODEL.md for what this protects against and what it does not.

Requirements

  • Node.js 22.13 or later (it uses the built-in node:sqlite).
  • Your own Google Cloud project with the Google Ads API enabled.
  • An OAuth client of type "Desktop app" in that project.
  • Explorer access to the Google Ads API, applied for in Google Cloud Console for that project. Until it is approved, only Google Ads test accounts work. No developer token is needed.

Step by step: docs/SETUP.md.

Install and connect

Always name the exact version (@0.2.0-alpha.1, not @latest or none), so npm can't hand you a different release. init asks for your OAuth client id and secret, opens Google's sign-in in your browser, saves the sign-in encrypted, writes a read-only starter policy, runs doctor, and prints the config for your AI app.

Or build from source:

sh
git clone https://github.com/vilovibe/perfoads-mcp.gitcd perfoads-mcppnpm install --frozen-lockfile && pnpm buildnode packages/cli/bin/perfoads-mcp.js init

init then prints your AI app's config for that copy (full path to node and to packages/cli/bin/perfoads-mcp.js). Wherever these docs say npx perfoads-mcp@<version> <command>, run that node command instead.

The AI app config (init prints it with your version filled in; for a source build it prints the node form):

Claude Desktop (Settings > Developer > Edit Config, claude_desktop_config.json), Cursor (~/.cursor/mcp.json):

json
{  "mcpServers": {    "perfoads-mcp": {      "command": "npx",      "args": ["-y", "perfoads-mcp@<version>", "serve"]    }  }}

Claude Code:

sh
claude mcp add --scope user perfoads-mcp -- npx -y perfoads-mcp@<version> serve

Or as a Claude Code plugin (it pins the release version; claude-plugin/):

/plugin install perfoads --marketplace vilovibe/perfoads-mcp

ChatGPT desktop app and Codex: they run local MCP servers too; see docs/SETUP.md.

More: examples/ (Claude Desktop, Claude Code, Cursor, a starter policy). A Claude Desktop extension (.mcpb) can be built with node scripts/build-mcpb.mjs; run init in a terminal first, the extension uses the same settings folder.

Then ask your AI app: "run doctor", "list my Google Ads accounts", "audit account 123-456-7890".

Quick tour

What to ask your AI, step by step: docs/USING.md. Full list with every input: docs/TOOLS.md (generated from the server).

  • Setup and accounts: doctor (checks the setup and says what to fix), list_accounts (incl. accounts under manager accounts).
  • Reads: get_account_overview, get_campaigns, get_ad_groups, get_keywords, get_search_terms, get_budgets_and_bidding, get_negatives, get_ads_and_assets, get_audiences, get_conversions, get_change_history, keyword_ideas, describe_resource, and query_gaql (one read-only GAQL SELECT). Summary first, then one page of rows and a cursor. Money is a decimal string in the account currency.
  • Checks: build_snapshot reads what the checks need into a local snapshot, run_checks runs them (findings with evidence; "not run" with the reason, never "clean" when data was missing), explain_check. Prompt weekly_audit. Catalog: docs/CHECKS.md.
  • Changes: propose_change, apply_change, undo_change, get_change, list_changes, resolve_change. Types: budgets, campaign status, bidding targets, negatives (campaign and shared lists), keywords, pausing keywords, responsive search ad text, sitelinks, callouts, audience exclusions, and creating a new Search campaign from plain fields (always made paused; undo only while it never ran).

Text that comes from Google or the AI (names, search terms, ad texts, errors) is wrapped as {"untrusted": "..."} so the AI treats it as data, not instructions.

How a change works

  1. Propose - the AI suggests a change. The server reads the live values, checks your policy, and asks Google to validate it without changing anything.
  2. Approve - the server opens a local approval page (127.0.0.1, one-time link, 10 minutes), or, with open_browser: false, you open it yourself with approvals open <id> in a terminal. You approve with its code, or reject.
  3. Apply - sent once. If the account changed since you approved, nothing is sent (stale). An unclear answer from Google is never re-sent: it is re-checked by reading the account.
  4. Read back - the server reads the account again to confirm the change landed (verified), or says loudly that it did not.
  5. Log and undo - every step is in the audit log (changes://log); undo_change proposes the reverse as a new change, with its own approval.

Approval paths and approval_mode: see docs/HARDENED-SETUP.md.

The policy file

policy.yaml in the settings folder decides which accounts the AI may read and change and how far: per-account spend limits in the account's currency, budget and target step limits, writes per minute and day, tiers per change type (auto, confirm, blocked), brand terms that are never negated, a Google Ads operations reserve for changes, and a kill switch. It is re-read on every check, and no tool can edit it.

sh
npx perfoads-mcp@<version> policy check   # is it valid, and what does it allow?npx perfoads-mcp@<version> policy show    # what the server uses now

Every key, range and default, with examples: docs/POLICY.md. Starter file: examples/policy.example.yaml.

What is stored, and how to delete it

Everything lives in one settings folder, never in your project: ~/.config/perfoads-mcp (or $XDG_CONFIG_HOME/perfoads-mcp), %APPDATA%\perfoads-mcp on Windows. Change it with PERFOADS_MCP_HOME, but keep it outside any folder the AI works in (doctor checks).

  • store.sqlite - your OAuth client id, client secret and refresh token (each encrypted, AES-256-GCM), the list of your accounts, proposed and applied changes, approval sessions (codes only as hashes), the audit log, Google Ads operations used, and every policy version loaded.
  • snapshots/ - account snapshots for the checks (gzipped JSON, owner-only, kept 24 hours, at most 20). They hold account data such as campaign names and search terms.
  • policy.yaml - your change policy. config.json - optional approval_mode, open_browser, retention_days and pro_plugin (an absolute path to a separately installed plugin package, loaded at start; a plugin runs as code with the server's rights, so set it only for a package you trust; it is refused when anyone but the user running the server, or root, can change its files or a folder above them; its text comes back wrapped as untrusted; doctor shows whether it loaded and why not).
  • Kept for 180 days by default ("retention_days" in config.json, 30 to 3650): when the server starts, changes in a final state (verified, failed, rejected, expired, stale, settled, abandoned), finished approval sessions, policy versions nothing still refers to, and the oldest audit log entries, all older than that, are deleted. Unfinished changes (proposed, approved, applying, in doubt, applied but not yet checked) and anything they need are never deleted. Before deleting, the server writes a retention_pruned entry to the audit log with what it removes and a checkpoint (the number and hash of the last entry removed), so the rest of the log still verifies. Snapshot temp files from interrupted saves go after 1 hour.
  • The encryption key: macOS Keychain, the Linux keyring (secret-tool), or a key file (key) in the settings folder (always the key file on Windows).
  • Owner-only, per OS: on macOS and Linux the store files, the key file, snapshot files and the starter policy.yaml are created 0600 and snapshot folders 0700 (a config.json you write yourself keeps the mode you give it). On Windows file modes do nothing, so each time the store opens the server sets an ACL on the whole settings folder with %SystemRoot%\System32\icacls.exe: inheritance from the parent removed, full control for your Windows user only, applied to everything already in it and inherited by everything created later (store, key file, snapshots, policy.yaml, config.json); the key file, snapshot folders and files and the starter policy get it again when they are written. doctor warns on Windows when the settings folder is outside %APPDATA%, and fails (with the fix) when icacls could not make it owner-only.

Tokens are encrypted at rest and never written to logs or shown in tool results. Encryption protects a copy of the folder (a backup, a synced folder). It does not protect against programs running as your own OS user: the Keychain entry, the keyring entry and the key file can be read by any process of the same user without a prompt, including an AI agent with a terminal. The hardened setup closes that, within its stated limits (human steps from a session the AI can't change).

Disconnect and delete: npx perfoads-mcp@<version> reset asks Google to revoke the saved sign-in (best effort; it says whether Google confirmed), then deletes the sign-in and its key (it asks first). Delete the settings folder to remove everything else. If reset says Google did not confirm, remove the app's access at myaccount.google.com/permissions.

Nothing is sent anywhere except to Google's APIs. No telemetry.

Commands

perfoads-mcp init                    guided setupperfoads-mcp auth                    sign in again (uses the saved OAuth client)perfoads-mcp doctor                  check the setupperfoads-mcp reset                   delete the saved sign-in and its key (asks first)perfoads-mcp serve                   run the MCP server over stdio (AI apps start this)perfoads-mcp approve <change_id>     approve with the code from the approval page (terminal only)perfoads-mcp reject <change_id>      reject a proposed change (terminal only)perfoads-mcp approvals open <id>     show a change's approval page from this terminalperfoads-mcp policy check [file]     check a policy file and say what it allowsperfoads-mcp policy show             the change policy the server uses now

Use npx perfoads-mcp@<version> <command> (or the command init printed) if it is not installed globally.

Known limits

  • Alpha. The Google Ads queries and writes are tested against recorded and fake replies. Live runs passed on a Google Ads test account (2026-10-25, every change type with its undo) and read-only on 4 real accounts (2026-10-26), and every change type with its undo on a real account (2026-11-01, $0 spent). Coverage per change type: docs/E2E-COVERAGE.md.
  • Same OS user, no hardened setup: an AI with a shell can read the store and key, start its own copy of the server, edit the policy, and fake a terminal (approve and approvals open are a convenience, not a guard). An AI that controls your browser can read the approval page, code included. Until you open it, the page link is visible for a moment in the browser opener's command line (ps) and stays in browser history; it stops working after the first open.
  • Access levels: Explorer access allows 2,880 operations a day on live accounts (15,000 on test accounts, counted separately) and no Keyword Planner (keyword_ideas needs Basic or Standard). The server keeps a share of each day's operations for approved changes.
  • OAuth app in "Testing" mode: Google ends the sign-in after 7 days. init and doctor warn; publish the app to production to avoid it (docs/SETUP.md).
  • Unclear outcomes: changes to portfolio bidding strategies and shared negative lists can't be settled from Google's change history; after an unclear answer they stay in_doubt until the new value shows, you settle them (resolve_change), or 30 days pass.
  • Change history reads at most 28 days. Every tool answers within about 4 minutes; very large accounts get samples, marked as such, and checks on a sample never say "clean".
  • Not supported yet: ChatGPT on the web (needs a remote server; the ChatGPT desktop app and Codex work), remote or team use, Merchant Center, billing.

Privacy Policy

PerfoAds MCP runs on your computer. Your Google Ads data goes only between your computer and Google; nothing is sent to PerfoAds, and there is no telemetry. What is stored locally, for how long, and how to delete it: What is stored, and how to delete it. Full policy: https://perfoads.com/privacypolicy

Terms of service: https://perfoads.com/termsofservice (the PerfoAds MCP section: free, as is, no warranty, Apache-2.0, your own Google account and costs, you approve every change).

Contact: [email protected] for privacy and security questions (security reports: see SECURITY.md). Bugs and support: https://github.com/vilovibe/perfoads-mcp/issues

Docs

Packages: ads-client, reads, snapshot, checks, changes, policy, store, mcp-server, cli (see packages/). Contributing: CONTRIBUTING.md.

Licence

Apache License 2.0. See LICENSE and NOTICE.

  • You are free to use, change, share and sell it, for any purpose.
  • Keep the licence and the NOTICE file with any copy you share, and say what you changed.
  • It comes with no warranty.

Google Ads is a trademark of Google LLC.

Source: README.md at commit ce171c8

Tools

25

doctor

Check setup

Checks the setup step by step: Node.js, where settings are stored, the saved Google sign-in, Google Ads API access level and limits, and risky Claude Code hooks. Each line says ok, warn, fail or skipped in plain English, with a fix; exact commands and file paths are in each check's details.

Takes no parameters.

list_accounts

List Google Ads accounts

Lists every Google Ads account this sign-in can work on, including accounts under manager (MCC) accounts, with currency, time zone and the login-customer-id each one needs.

Parametersrefresh

query_gaql

Run a read-only GAQL query

Runs one Google Ads Query Language SELECT on one account. Read-only: anything but a single SELECT on a known resource is refused. LIMIT is added (200) or lowered (max 1000). Returns a summary first, then up to max_rows rows; next_cursor fetches the rest. Costs 1 Google Ads operation per call.

Parametersqueryrequiredcursormax_rowscustomer_idrequired

get_account_overview

Account overview

One account at a glance: name, currency, time zone, totals for the date range (cost, clicks, conversions, value, ROAS), campaigns per channel. Example: {"customer_id":"123-456-7890"}

Parametersdate_rangecustomer_idrequired

get_campaigns

Campaigns

Campaigns that are not removed, plus removed ones that had activity in the date range: status, channel, budget, bidding, targets, metrics, Search impression share; highest cost first. Example: {"customer_id":"123-456-7890","date_range":"LAST_7_DAYS"}

Parameterscursorstatusmax_rowsdate_rangecampaign_idcustomer_idrequired

get_ad_groups

Ad groups

Ad groups (not removed): status, type, CPC bid, metrics; highest cost first. Example: {"customer_id":"123-456-7890","campaign_id":"111"}

Parameterscursorstatusmax_rowsdate_rangead_group_idcampaign_idcustomer_idrequired

get_keywords

Keywords

Keywords with impressions: match type, quality score parts, bids, page CPC estimates, metrics; highest cost first. Example: {"customer_id":"123-456-7890","ad_group_id":"222"}

Parameterscursorstatusmax_rowsdate_rangead_group_idcampaign_idcustomer_idrequired

get_search_terms

Search terms

Search terms with clicks (Search + Performance Max) with triggering keyword, status and metrics; highest cost first; cost on terms with no conversions. Example: {"customer_id":"123-456-7890","date_range":"2026-09-01..2026-09-30"}

Parameterscursormax_rowsdate_rangead_group_idcampaign_idcustomer_idrequiredinclude_pmax_categories

get_budgets_and_bidding

Budgets and bidding

Budgets with the campaigns sharing each, and portfolio bid strategies with their targets and campaigns: what a budget or strategy change touches. Example: {"customer_id":"123-456-7890"}

Parameterscursormax_rowscustomer_idrequired

get_negatives

Negatives

Negative keywords on ad groups and campaigns, shared negative lists (keywords, links) and account-level negatives and exclusions. Example: {"customer_id":"123-456-7890"}

Parameterscursormax_rowscustomer_idrequired

get_ads_and_assets

Ads and assets

Ads (RSA texts with pinning and labels, URLs, ad strength, policy) and asset links (sitelinks, callouts, snippets, images...) on account, campaigns, ad groups. Example: {"customer_id":"123-456-7890","kind":"ads"}

Parameterskindcursorstatusmax_rowsdate_rangead_group_idcampaign_idcustomer_idrequired

get_audiences

Audiences

User lists (type, status, size, targeted or not) and audience targeting/exclusions on campaigns and ad groups, with metrics. Example: {"customer_id":"123-456-7890"}

Parameterskindcursormax_rowsdate_rangead_group_idcampaign_idcustomer_idrequired

get_conversions

Conversions

Conversion actions (status, category, primary, counting, attribution, values) with conversions in the range; account/campaign goals; value rules. Example: {"customer_id":"123-456-7890"}

Parameterscursormax_rowsdate_rangecustomer_idrequired

get_change_history

Change history

Who changed what, when (1-28 days; Google keeps 30): resource, operation, fields, tool; users as "user at domain". Example: {"customer_id":"123-456-7890","days":7}

Parametersdayscursormax_rowscampaign_idcustomer_idrequiredresource_type

keyword_ideas

Keyword ideas

Keyword Planner ideas from seed keywords and/or a URL: searches, competition, bids. Needs Basic/Standard access. Example: {"customer_id":"123-456-7890","keywords":["trail running shoes"]}

Parametersurlkeywordslanguagecustomer_idrequiredmax_resultsgeo_target_idsinclude_search_partners

describe_resource

Describe a GAQL resource

Fields of one GAQL resource from Google's field catalogue (type, selectable, filterable, sortable, enum values) plus the metrics and segments you can select with it. Example: {"resource":"campaign"}

Parameterscursormax_rowsresourcerequired

build_snapshot

Build account snapshot

Reads everything the checks need (90 days) into a saved snapshot (kept 24 h; a complete one under 60 min old is reused unless refresh:true). Returns its id and how each read went; failed or cut reads are said. Then run_checks. Example: {"customer_id":"123-456-7890"}

Parameterschecksrefreshcustomer_idrequired

run_checks

Run account checks

Runs the checks on a snapshot (snapshot_id) or on customer_id (reuses one under 60 min old unless refresh). Findings first, with evidence and severity, paged by next_cursor; after the last finding: info (not faults), clean, not_run (why) and suppressed (why). For an audit, call again with next_cursor until it is null and report every finding in the audit report format (server instructions, weekly_audit prompt), never only the main points. A keyword id looks like "<ad_group_id>~<criterion_id>"; pause_keyword takes it as two fields (ad_group_id, criterion_id). Example: {"snapshot_id":"snap_..."}

Parameterscursordetailrefreshsettingsbrand_termscustomer_idsnapshot_idclient_context

explain_check

Explain a check

What one check looks for, the data it needs, its settings with defaults, and when it cannot run. Tell the user in plain words what it looks for and what it needs; do not suggest settings overrides or how to fix things. Example: {"check_id":"BB02"}

Parameterscheck_idrequired

propose_change

Propose a change

Builds one change, checks it with Google (validate only), never applies. params schema per type: schema://changes/{type}. Returns diff, risk, change_id; then apply_change.

Parameterstyperequiredparamsrequiredreasoncustomer_idrequired

apply_change

Apply an approved change

Sends a proposed change once a person approves it: opens an approval page in the user's browser (approve there, or type its code when asked), sends once, reads back.

Parameterschange_idrequired

undo_change

Propose an undo

Proposes a change restoring the values from before an applied change. Needs its own approval via apply_change.

Parameterschange_idrequired

get_change

Get a change

One change: status in plain words, diff, approval, result. An in_doubt change is re-checked by reading.

Parameterschange_idrequired

list_changes

List changes

Recent changes, newest first: id, type, status in plain words, account, a one-line summary and times. get_change shows one change in full.

Parameterslimitstatuscustomer_id

resolve_change

Settle an unclear change

Settles an in_doubt change after the user checked Google Ads (outcome + note). A person confirms on the approval page or types its code. Sends nothing.

Parametersnoterequiredoutcomerequiredchange_idrequired

Version history

1
  1. v0.2.0-alpha.1LatestNov 8, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts
  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts