Engineering Case ยท MCP Server Design ยท Google Ads + Pinterest ยท Production

Designing a Custom MCP Server from Official Ad API Endpoints

Two purpose-built MCP servers that let an AI agent operate a live advertising account. The interesting work was not wiring up the protocol, which is straightforward. It was deciding what the tools should be: reading the official Google Ads and Pinterest API references end to end, working out which operations an agent genuinely needs, and collapsing a long endpoint list into a short set of tools that carry the account's own rules inside them.

18 toolsGoogle Ads MCP server over API v23
9 toolsPinterest Ads MCP server over API v5
3 levelsTool surface mirrors campaign, ad group, ad
2 referencesOfficial API docs analysed to design the surface

An off-the-shelf MCP server mirrors the API

The first version of this setup used a publicly available MCP server for Google Ads. It works, and for a general audience its design is the correct one: it exposes the advertising API more or less as it is, one tool per operation, each taking the platform's native request objects.

That means the agent receives a long list of low level methods named after the mutations they wrap, and has to compose the right sequence itself on every task. It also means the server has no opinion about what a good campaign looks like, because it cannot have one.

Running an account that way surfaced the real problem quickly. The agent reported success consistently, because the only signal available to it was whether the API accepted a call.

Working from the endpoint reference

The design work started in the official documentation for both platforms rather than in code:

  1. Inventory the endpoints and their exact request and response schemas, including the reporting fields each one accepts.
  2. List the operations that actually get performed on the account in a normal week, in plain language.
  3. Map the second list onto the first. Some intents map to one endpoint. Several map to a chain of calls. Many endpoints map to nothing anyone needs.
  4. Expose one tool per intent, and keep the chains, the field selection and the payload assembly inside the server.

The mismatch between those two lists is the whole design. An API is organised by resource, because that is the sane way to publish one. Work is organised by intent.

The tool surface mirrors the ad hierarchy, not the endpoint list

An advertising account has a natural shape: campaigns contain ad groups, ad groups contain ads and keywords. Every real task lands on one of those levels. Shaping the tools the same way means the agent never has to translate between two different mental models, and it makes the surface small enough to reason about.

Campaign level
  โ†“ audit against the quality checklist, budget, targeting, bid ceiling, impression share
Ad group level
  โ†“ keywords, negative shields, collision detection across the whole account
Ad level
  โ†“ creative validation, asset coverage, deployment
Every write path
  โ†“ validate against business rules โ†’ create paused โ†’ wait for human approval

Tool names follow the intent rather than the mutation. audit_campaign_4level_shield runs the full quality checklist over a campaign. mine_search_terms_and_harvest_negatives pulls the search query report, classifies terms and returns the negatives worth applying. auto_fix_collisions_and_dupes looks across campaigns for the same keyword competing with itself. Each one hides several API calls and a set of rules.

Choosing columns instead of forwarding everything

Pinterest exposes campaign analytics through a reporting endpoint that takes a catalogue of available columns. Handing an agent that catalogue and asking it to choose each time would be slow, inconsistent and expensive in context.

The tool pins the set instead. get_pinterest_analytics_report always requests exactly nine columns: campaign id and name, spend, paid impressions, clickthroughs, outbound clicks, saves, total engagement and click-through rate.

One request from the agent also fans out into two calls: fetch the campaigns to collect their ids, then request analytics for those ids over the chosen date range.

The server also does the unit conversion. Spend arrives from the API in micro units and is turned into currency before the agent ever sees it, which removes an entire category of silent arithmetic mistakes.

Encoding an invariant the API does not enforce

Google Ads mutations take a field mask that tells the API which fields to update. Omit it while changing a bid and the call still succeeds. The platform simply applies its fallback and the bid quietly drops to its minimum, which takes the keyword out of the auction.

Nothing about that response looks like a failure, so an agent has no way to notice it.

In the server every write path that touches a bid or a status builds the mask from the object being updated, using protobuf_helpers.field_mask, before the operation is sent. Three write paths carry it.

The agent is never asked to remember this. It cannot express the broken version of the call, because the tool does not offer it.

The rule that came out of it

An agent optimises against whatever definition of success its tools give it. If the only available signal is the API accepted this call, that is what gets optimised, and the report will say everything is fine. Moving the definition of correct into the tool surface turns a class of expensive mistakes into calls that cannot be made at all.

Why build a custom MCP server instead of using the ready-made one

The public server is well made and it is honest about what it is: a faithful mirror of the advertising API. For general use that is exactly right, and for a while it was enough here too.

Generality becomes the constraint at the point where an account acquires standards of its own. A shared, public tool has no way of knowing what counts as a correct ad in your business, so it cannot refuse an incorrect one. Five specific gaps made the case for building a replacement.

1. Level of abstraction

Low level methods named after API mutations put the burden of composing a correct sequence on the agent, every time. The same task can be assembled differently on different days, which makes behaviour hard to reason about and harder to trust.

2. Invariants the platform does not check

The field mask case above is the clearest example. A general purpose server cannot forbid that call, because without knowing the intent it cannot tell a mistake from a deliberate reset. A facade built for one account knows the intent and can refuse.

3. Knowledge of the business

Creative standards, link formats, product constraints and promotional rules exist only in the account they belong to. A public server has no access to any of it, so validation against those rules has to live somewhere else, which in practice means it lives in a prompt and gets forgotten.

4. No cross-campaign view

Endpoints operate on one object at a time. Keyword cannibalisation exists between objects, so no single call can see it. Detecting it requires a tool designed to look across the account, which is a shape the API reference does not suggest.

5. The cost of context

Passing whole platform request and response structures through the agent's context on every action makes routine work slow and expensive. With a facade the agent sends an identifier and some copy, and the server assembles the payload.

Off-the-shelf MCP server versus a purpose-built facade

Dimension Off-the-shelf MCP server Purpose-built facade Consequence when it goes wrong
Abstraction level Raw API. Many low level methods named after the mutation they wrap. Business operations such as audit_campaign_4level_shield and mine_search_terms_and_harvest_negatives. Effort goes into plumbing rather than into the account.
Platform quirks Unguarded. A bid mutation without a field mask succeeds and silently resets the bid to the 0.01 EUR minimum. The mask is built inside the server on every write path. A keyword drops out of the auction and nothing reports an error.
Business rules Unknown to the server by design. Encoded in the tool and checked on every write. Ads go live that contradict the actual offer.
Creative validation Will publish a partial ad with a poor strength rating. Hard gate at 15 headlines and 4 descriptions in the target language. Weaker ad strength raises the price of the same placement.
Cross-campaign checks No concept of overlap between your own campaigns. auto_fix_collisions_and_dupes scans the account and clears duplicates. Your campaigns bid against each other in the same auction.
Context cost Full platform payloads travel through the agent's context on every call. Identifiers and copy in, assembled payload built server side. Every routine operation is slower and more expensive.

Paused by default

Every tool that creates or changes something able to spend money produces it in a paused state. New campaigns, new ad groups, bid adjustments: prepared, validated, inert until a person enables them.

The reasoning is specific. The realistic failure mode for a capable agent with write access is not a rogue decision, it is a confident action taken on a misread instruction. A hypothetical question in a chat window should never be able to become a live change to a real budget, and a paused-by-default policy makes that structurally impossible rather than unlikely.

Alongside that, every decision, test and adjustment is written to a dated log, so the account carries an audit trail instead of a black box.

What this changed in practice

With the rules living in the tools, the recurring account work became routine instead of occasional: search query audits with negatives applied before the next spend cycle, cross-campaign collision checks, creative compliance enforced before an ad can run, and both channels answering in the same shape.

Adding another platform is now a matter of adding a server rather than reworking the whole setup. Google Analytics 4 and Meta Ads are the next candidates.

One control panel for every ad channel: the same story for a non-technical reader โ†’

Tools used

Python MCP Model Context Protocol Google Ads API v23 Pinterest Ads API v5 Protobuf field masks Obsidian Claude Code
Need an agent that can safely touch a production system?

I design tool surfaces for AI agents working against real APIs, where the interesting part is which operations to expose and which invariants to enforce. Happy to talk through a specific system.

Book a 30 min call Email me All projects
โ† Back to projects