Apple Ads MCP Server
Provides integration with the Apple Ads Platform API, enabling management of campaigns, ad groups, keywords, creatives, budgets, reports, and other advertising resources for Apple Search Ads and Apple Maps brand ads.
Allows searching the App Store catalog, checking app-store ad eligibility, and retrieving app locale details and metadata for use in advertising campaigns.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Apple Ads MCP Servershow me my active campaigns' spend and impressions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Apple Ads MCP Server
The Apple Ads Platform API, as an MCP server. 99 operations across every documented resource, exposed to Claude Code, Claude Desktop, Cursor and any other Model Context Protocol client through 4 token-efficient tools.
apple-ads-mcp is an open source Model Context Protocol (MCP) server for the Apple Ads Platform API — the API behind Apple Search Ads and Apple Maps brand ads. Install it once and your AI agent can read and write campaigns, ad groups, keywords, creatives, budgets, reports and everything else Apple exposes for your ad accounts.
The catalog is generated from openapi/apple-ads.openapi.json, reconstructed from Apple's official Node client (apple/apple-ads-platform-api-node, spec tag 109) and cross-checked endpoint by endpoint against Apple's published documentation.
No credentials live in this repository. You supply your own Apple Ads API credentials at runtime through environment variables. Nothing is bundled, logged or transmitted anywhere except Apple.
Table of contents
Why 4 tools instead of 99
Registering all 99 Apple Ads operations as individual MCP tools means dumping 99 full JSON Schemas into the model's context window before it does any real work. Most of that budget is wasted on operations the agent never calls in a given conversation.
This server keeps the full catalog internal and exposes a small, stable surface on top of it:
ads_search → find operations (names, methods, paths, tags — no schemas)
ads_schema → describe one operation (full input JSON Schema + call hint)
ads_call → execute one operation (real request, or _dryRun)
ads_tags → list resource tags (with operation counts, to narrow search)The agent pays for exactly the schema it is about to use, and nothing else. A typical search → schema → call flow costs a few thousand tokens instead of the tens of thousands it would take to load every schema up front — and it loses no coverage, because the full 99-operation catalog is still reachable through ads_search and ads_call.
Related MCP server: mlg-meta-mcp
What you can do with it
All 99 operations across 28 resource tags. Run ads_tags at any time for the live, exact list with counts.
Campaigns — create, read, update, delete, query by filter/sort/pagination (
Campaigns)Ad groups — create, read, update, delete, query (
AdGroups)Keywords and negative keywords — create, read, update, delete, query, plus bulk create/update for both (
Keywords,NegativeKeywords)Ads — create, read, update, delete, query (
Ads)Creatives — create, read, update, delete, query (
Creatives)Assets — upload, read, delete, query — the images and video used in creatives and Apple Maps brand ads (
Assets)Product pages — read custom product pages and their locale details, query (
ProductPages)Shared budgets — create, read, update, delete, query — budgets shared across multiple campaigns (
SharedBudgets)Geo and locations — search and query targetable locations, manage location groups (
Locations,LocationGroups,Search)Apple Maps brand ads — business brands and business categories for Maps-based campaigns (
Brands,Categories)Reports — app and business-brand performance reports by campaign, ad group, ad, keyword and search term (
Reports)Insights — impression share and search-term popularity (
Insights)Recommendations — daily budget and target CPA recommendations: query, apply, dismiss (
Recommendations)Suggestions — keyword, phrase, category and target-CPA suggestions for building out campaigns (
Suggestions)Change history — audit summaries and change details for an ad account (
ChangeHistory)Account and access management — ad accounts, org info, user ACLs, the authenticated caller's identity, advertiser resources (
AdAccounts,Orgs,Acls,Me,AdvertiserResources)App search, eligibility and metadata — search the App Store catalog, check app-store-ad eligibility, look up app locale details, supported languages, and rejection reasons for apps and brands (
Apps,Search,Eligibilities,Metadata,RejectionReasons)
Requirements
Bun 1.1 or newer
An Apple Ads account with API access — a client ID, team ID, key ID and a private key, from the Apple Ads UI
An MCP client that speaks stdio: Claude Code, Claude Desktop, Cursor, or your own agent
Getting your credentials
Sign in at ads.apple.com.
Generate an EC private key locally (Apple never sees the private key itself):
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out private-key.pem openssl ec -in private-key.pem -pubout -out public-key.pemGo to Account Settings → API, paste the contents of
public-key.pem(including theBEGIN/ENDlines) into the Public Key field, and save.After saving, the page shows your credentials block: a Client ID, a Team ID, and a Key ID. Copy all three.
Keep
private-key.pemoutside your repository, for example~/.apple-ads/private-key.pem, andchmod 600it.
This server takes it from there: it signs the ES256 client-secret JWT, exchanges it for an access token, and caches the token in memory. See Ad account scoping for how to find your ad account ID once you have credentials.
Install and configure
git clone https://github.com/imfaisii/apple-ads-mcp.git
cd apple-ads-mcp
bun install
bun run smoke # offline catalog check + server boot — no credentials neededCopy .env.example to .env and fill in your values, or pass them directly in your MCP client's config. Any client that launches a stdio MCP server works the same way — mcp.example.json in this repo is a ready-to-copy template:
{
"mcpServers": {
"apple-ads": {
"command": "bun",
"args": [
"run",
"/ABS/PATH/to/apple-ads-mcp/src/index.ts"
],
"env": {
"APPLE_ADS_CLIENT_ID": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"APPLE_ADS_TEAM_ID": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"APPLE_ADS_KEY_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"APPLE_ADS_PRIVATE_KEY_PATH": "/ABS/PATH/to/private-key.pem",
"APPLE_ADS_AD_ACCOUNT_ID": "1234567"
}
}
}
}Claude Code — register it globally:
claude mcp add apple-ads \
-s user \
-t stdio \
-e APPLE_ADS_CLIENT_ID=SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
-e APPLE_ADS_TEAM_ID=SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
-e APPLE_ADS_KEY_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
-e APPLE_ADS_PRIVATE_KEY_PATH=$HOME/.apple-ads/private-key.pem \
-e APPLE_ADS_AD_ACCOUNT_ID=1234567 \
-- bun run /ABS/PATH/to/apple-ads-mcp/src/index.ts
claude mcp get apple-adsStart a new session. The tools appear as mcp__apple-ads__ads_search, …ads_schema, …ads_call and …ads_tags.
Claude Desktop — add the same mcpServers block shown above to claude_desktop_config.json:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
Restart Claude Desktop after editing it.
Cursor — ~/.cursor/mcp.json (global) or .cursor/mcp.json (per project), same mcpServers block.
Your own agent — run bun run src/index.ts and speak MCP over stdio. It will look like it is hanging in a raw terminal; that is a stdio server waiting for a client, and it is correct.
The four tools
ads_search — find the right operation
Free-text search over operationId, name, path, tags, method and description. Returns ranked hits without the JSON Schemas.
Parameter | Type | Required | Description |
| string | optional* | Free text, e.g. |
| string | optional* | Exact resource tag filter, e.g. |
| string | optional |
|
| integer | optional | Max hits, default |
* At least one of query, tag or method.
{ "query": "create campaign", "method": "POST", "limit": 5 }ads_schema — describe one operation
Returns the operation's full input JSON Schema, its path/query/header parameter lists, and a callHint showing how to invoke it.
Parameter | Type | Required | Description |
| string | yes | OperationId or generated tool name, e.g. |
{ "operation": "campaigns_create" }ads_call — execute the operation
Parameter | Type | Required | Description |
| string | yes | OperationId or generated tool name |
| object | optional | Path params, query params, |
| boolean | optional | Resolve method/path/query/body without calling Apple |
Path and query fields, adAccountId, and _dryRun all work either nested inside args or as top-level keys next to operation.
// query campaigns
{
"operation": "campaigns_query",
"args": {
"body": {
"filters": [{ "field": "status", "operator": "EQUALS", "value": "ENABLED" }],
"pagination": { "pageSize": 10, "fetchTotalCount": true }
}
}
}
// create a campaign
{
"operation": "campaigns_create",
"args": {
"body": {
"name": "Away Finder — Brand",
"billingEvent": "TAPS",
"startTime": "2026-09-01T00:00:00Z",
"promotedObjectType": "APPSTORE_APP",
"promotedObjectId": "1234567890",
"status": "ENABLED",
"dailyBudget": { "value": { "amount": "50.00", "currency": "USD" } }
}
}
}ads_tags — orient inside the API
Parameter | Type | Required | Description |
| integer | optional | Max tags, default all 28, sorted by operation count |
{}A worked example
A full flow from zero context to a real call: discover the ad account, find the operation, read its schema, then run it.
1. ads_call { operation: "acls_list" }
→ { result: { acls: [ { adAccount: { id: 123456789, name: "Away Finder" }, roles: ["Admin"] } ] } }
Use adAccount.id as the ad account for every scoped call below.
2. ads_search { query: "list campaigns" }
→ hits: [ { name: "campaigns_query", operationId: "campaignsQueryPost", method: "POST", path: "/campaigns/query", tags: ["Campaigns"] }, ... ]
3. ads_schema { operation: "campaigns_query" }
→ full inputSchema (adAccountId, body.filters, body.sorting, body.pagination) + callHint
4. ads_call {
operation: "campaigns_query",
args: {
adAccountId: "123456789",
body: {
filters: [{ field: "status", operator: "EQUALS", value: "ENABLED" }],
sorting: [{ field: "name", order: "ASC" }],
pagination: { pageSize: 10, fetchTotalCount: true }
}
}
}
→ { status: 200, body: { result: [ { id: 542370549, name: "Away Finder — Brand", status: "ENABLED", ... } ], pagination: { offset: 0, pageSize: 10, totalCount: 1 } } }To create something instead of listing it, run the same shape as a dry run first (see Dry run), check the resolved request against ads_schema, then call it for real.
Ad account scoping
Most Apple Ads calls are scoped to one ad account. The server sends this as the X-Ap-Context header:
X-Ap-Context: adAccountId=123456789;You do not build this header yourself. Either:
set
APPLE_ADS_AD_ACCOUNT_IDonce in your environment, so every scoped call uses it by default, orpass
adAccountIdper call insideargs(or as a top-level field next tooperation), which overrides the default for that one call.
A handful of operations do not need an ad account and work with just your access token: me_list (GET /me) and acls_list (GET /acls). Call acls_list first — it returns every ad account your credentials can reach, plus your role on each, so you know which adAccountId values are valid before scoping any other call to one.
Query, pagination and sorting
Most list-style endpoints follow the same shape: POST /<resource>/query with a selector body of filters, sorting and pagination. This is the same pattern whether you're querying campaigns, ad groups, keywords, creatives, or a report.
// POST /campaigns/query
{
"filters": [
{ "field": "status", "operator": "EQUALS", "value": "ENABLED" }
],
"sorting": [
{ "field": "name", "order": "ASC" }
],
"pagination": {
"offset": 0,
"pageSize": 10,
"fetchTotalCount": true
}
}Filters (field, operator, value, optional ignoreCase) support a wide operator set: EQUALS, NOT_EQUALS, IN, NOT_IN, CONTAINS_ANY, CONTAINS_ALL, NOT_CONTAINS_ANY, NOT_CONTAINS_ALL, STARTS_WITH, ENDS_WITH, LIKE, NOT_LIKE, BETWEEN, GREATER_THAN, GREATER_THAN_OR_EQUAL_TO, LESS_THAN, LESS_THAN_OR_EQUAL_TO, IS_NULL, IS_NOT_NULL. Not every field on every entity supports every operator — check the field list in ads_schema for the operation you're calling.
Sorting takes field and order (ASC or DESC). Omit it and results sort by id ascending.
Pagination takes offset and pageSize, with fetchTotalCount (default false) to also return the total match count. Reporting endpoints cap pageSize at 5000 and default to 100 when omitted; other /query endpoints don't document a fixed cap — start with a modest pageSize and page through with offset.
Keywords and negative keywords also have bulk endpoints (keywords_bulkCreate, keywords_bulkUpdate, negativeKeywords_bulkCreate, negativeKeywords_bulkUpdate) that take an items array, each with a client-supplied correlationId and a data payload, plus an allowPartialSuccess flag.
Reports
The Reports tag covers 10 operations: app-level and business-brand-level performance reports by campaign, ad group, ad, keyword and search term (reports_appsCampaignsQuery, reports_appsAdgroupsQuery, reports_appsAdsQuery, reports_appsKeywordsQuery, reports_appsSearchtermsQuery, and the equivalent reports_businessBrandsCampaignsQuery / reports_businessBrandsAdgroupsQuery / reports_businessBrandsAdsQuery / reports_businessBrandsKeywordsQuery / reports_businessBrandsSearchtermsQuery for Apple Maps brand ads).
Every report operation is a POST /reports/.../query call taking the same filters / sorting / pagination selector body described above, scoped to an ad account:
{
"operation": "reports_appsCampaignsQuery",
"args": {
"adAccountId": "123456789",
"body": {
"pagination": { "pageSize": 100 }
}
}
}Run ads_schema { operation: "reports_appsCampaignsQuery" } first to see the exact filterable/groupable fields for that report before calling it — they differ per report type.
Uploading creative assets
assets_upload is the one operation that sends multipart/form-data instead of JSON. An MCP client can only send JSON, so the file part is described as an object and the server turns it into a real multipart upload:
{
"operation": "assets_upload",
"args": {
"adAccountId": "123456789",
"body": {
"file": { "path": "/Users/you/creative/hero.png", "contentType": "image/png" },
"promotedObjectId": "987654",
"promotedObjectType": "BUSINESS_BRAND"
}
}
}Use { "path": "/abs/path" } to read a file from the machine running the server, or { "base64": "...", "filename": "hero.png", "contentType": "image/png" } when the bytes are already in hand. Apple accepts PNG, JPG and HEIC here.
Errors and rate limits
Apple's response body is passed through intact. On a
4xx/5xx, the agent sees Apple's ownerror.code,error.messageanderror.details, so it can read exactly what was rejected and self-correct.On
401, this server refreshes the access token once and retries automatically — you shouldn't see stale-token errors surface to the agent.On
429, the response includes ahintplus the rate-limit response headers Apple sent (RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset, andRetry-Afterwhen present). The server does not retry on your behalf — back off using those headers, preferringRetry-Afterwhen it's present. Apple's docs don't publish a fixed numeric limit, so don't hardcode one; read the headers on each response instead.A bulk request (
keywords_bulkCreate, etc.) counts as a single call against the rate limit no matter how many items it carries — batch changes into bulk requests when operating at scale.Responses over ~120,000 characters are truncated with a hint to narrow the request. For list/report endpoints, lower
pageSizeor add more specificfiltersinstead of pulling everything at once.
Dry run
Every ads_call accepts _dryRun: true. It resolves the request — method, path, path params, query, headers, body, and the ad account context — without sending anything to Apple:
{
"operation": "campaigns_update",
"_dryRun": true,
"args": {
"id": "542370549",
"adAccountId": "123456789",
"body": { "status": "PAUSED" }
}
}{
"dryRun": true,
"operationId": "campaignsIdPut",
"method": "PUT",
"path": "/campaigns/{id}",
"pathParams": { "id": "542370549" },
"query": {},
"headers": {},
"body": { "status": "PAUSED" },
"adAccountId": "123456789",
"xApContext": "adAccountId=123456789;"
}Use this to check a write request against ads_schema before it touches a live ad account.
Environment variables
Variable | Required | Purpose |
| yes | Client ID from the Apple Ads UI credentials block |
| yes | Team ID from the same credentials block |
| yes | Key ID from the same credentials block |
| yes* | Absolute path to your EC private key file |
| yes* | Inline PEM contents, instead of a file path (useful in CI) |
| no | Default ad account ID for |
| no | Defaults to |
| no | Defaults to |
| no |
|
* Supply exactly one of APPLE_ADS_PRIVATE_KEY_PATH or APPLE_ADS_PRIVATE_KEY.
Copy .env.example to .env for local shells. .env*, *.p8 and *.pem are already in .gitignore.
How the catalog is generated
openapi/apple-ads.openapi.json → bun run generate → generated/tools.json
generated/manifest.jsonopenapi/apple-ads.openapi.json is the vendored source of truth: reconstructed from Apple's official Node client (apple/apple-ads-platform-api-node, spec tag 109) and cross-checked endpoint by endpoint against Apple's published documentation. scripts/generate-tools.ts reads it and emits one catalog entry per path+method — 99 operations, 80 paths, 28 tags — into generated/tools.json, plus counts and provenance into generated/manifest.json.
Tool descriptions are enriched from Apple's own documentation. generated/docs.json maps every METHOD /path to the title, abstract and URL of its page on developer.apple.com, so ads_search and ads_schema return Apple's own wording and a link you can open.
bun run generate # rebuild the catalog from openapi/apple-ads.openapi.json
bun run docs # refresh generated/docs.json from developer.apple.com
bun run smoke # regenerate, validate the catalog, boot the serverCoverage is verified, not assumed. Apple publishes 99 endpoint pages; this catalog has 99 operations, and every one matches 1:1 by method and path shape. The full endpoint-by-endpoint comparison is in docs/endpoint-coverage.md.
generated/ is build output. Never hand-edit it — change the spec or the generator, then regenerate. CI fails if the committed catalog doesn't match a fresh generation.
Escape hatch: every operation as its own tool
export APPLE_ADS_EXPOSE_ALL_TOOLS=1 # also register all 99 operations as individual MCP toolsLeave this unset in normal use — it exists for debugging the generated catalog, not for everyday agent use, and it puts every operation's full schema back into context.
Security
Nothing secret is stored in this repository. Credentials come from environment variables only.
.gitignoreblocks*.p8,*.pem,PrivateKey_*.p8and.env*.The client secret is a short-lived-by-policy ES256 JWT you sign locally with your private key (valid up to 180 days, Apple's own maximum); Apple never receives the private key itself. The resulting access token is cached in memory only, and is refreshed with a 60-second margin before the
expires_inApple returns (currently 3600 seconds) — plus a one-time retry on a401.Tool results never echo credentials back to the model.
The only network destinations are
APPLE_ADS_BASE_URL(defaults to Apple's API host) andAPPLE_ADS_AUTH_BASE_URL(defaults to Apple's OAuth host).If you ever commit a private key by accident, rotate it in the Apple Ads UI immediately.
See SECURITY.md for reporting a vulnerability.
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md. Two rules matter most: generated/ is regenerated rather than edited by hand, and no credentials ever appear in a pull request.
License
MIT © imfaisii
Not affiliated with or endorsed by Apple Inc.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceA Model Context Protocol server that exposes the entire Apple App Store Connect API (1,200+ operations) as MCP tools, enabling AI assistants to query apps, manage builds, handle submissions, read analytics, and more.21MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for Meta Ads providing 30 tools for account discovery, campaign management, targeting research, and insights. Designed with LLM-friendly outputs and productivity features like cloning and bulk operations.1MIT
- FlicenseNot gradedqualityBmaintenanceAn MCP server that exposes the Apple App Store Connect API to AI agents, enabling management of apps, metadata, in-app purchases, subscriptions, TestFlight, provisioning, reviews, analytics, and more through 113 curated tools plus two generic JSON:API escape-hatch tools.1
- AlicenseNot gradedqualityBmaintenanceComplete Google Ads API v21 MCP server with 40+ fully implemented tools for campaign, ad group, keyword, extension, and portfolio bidding management, enabling AI assistants to create, optimize, and manage Google Ads campaigns through natural language commands.MIT
Related MCP Connectors
Hosted MCP with 91 agent tools: X, domains, SEO, Maps, Trends, Search, YouTube, TikTok, and more.
Free public MCP for AI agents — 193 tools, 44 workflows. No API key.
60+ Meta Ads tools for AI agents: audits, campaign management, audiences and CAPI tracking.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/imfaisii/apple-ads-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server