Apple Ads MCP Server
An MCP server for the Apple Ads Platform API that exposes 99 operations across all resources through four token-efficient tools. It enables AI agents to read and write campaigns, ad groups, keywords, creatives, budgets, reports, and more.
README
<div align="center">
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.
</div>
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
- What you can do with it
- Requirements
- Getting your credentials
- Install and configure
- The four tools
- A worked example
- Ad account scoping
- Query, pagination and sorting
- Reports
- Uploading creative assets
- Errors and rate limits
- Dry run
- Environment variables
- How the catalog is generated
- Escape hatch: every operation as its own tool
- Security
- Contributing
- License
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.
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.pem - Go 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 needed
Copy .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-ads
Start 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.json - Windows:
%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 |
|---|---|---|---|
query |
string | optional* | Free text, e.g. "list campaigns", "create ad group", "keyword bids", "impression share report" |
tag |
string | optional* | Exact resource tag filter, e.g. Campaigns, AdGroups, Keywords |
method |
string | optional | GET, POST, PUT, DELETE |
limit |
integer | optional | Max hits, default 15, max 50 |
* 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 |
|---|---|---|---|
operation |
string | yes | OperationId or generated tool name, e.g. campaigns_create |
{ "operation": "campaigns_create" }
ads_call — execute the operation
| Parameter | Type | Required | Description |
|---|---|---|---|
operation |
string | yes | OperationId or generated tool name |
args |
object | optional | Path params, query params, body, adAccountId, _dryRun. Additional properties allowed |
_dryRun |
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 |
|---|---|---|---|
limit |
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, or - pass
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 |
|---|---|---|
APPLE_ADS_CLIENT_ID |
yes | Client ID from the Apple Ads UI credentials block |
APPLE_ADS_TEAM_ID |
yes | Team ID from the same credentials block |
APPLE_ADS_KEY_ID |
yes | Key ID from the same credentials block |
APPLE_ADS_PRIVATE_KEY_PATH |
yes* | Absolute path to your EC private key file |
APPLE_ADS_PRIVATE_KEY |
yes* | Inline PEM contents, instead of a file path (useful in CI) |
APPLE_ADS_AD_ACCOUNT_ID |
no | Default ad account ID for X-Ap-Context. Discover valid values with acls_list |
APPLE_ADS_BASE_URL |
no | Defaults to https://api.ads.apple.com/v1 |
APPLE_ADS_AUTH_BASE_URL |
no | Defaults to https://appleid.apple.com |
APPLE_ADS_EXPOSE_ALL_TOOLS |
no | 1 or true registers all 99 operations as individual MCP tools. Debug only |
* 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.json
openapi/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 server
Coverage 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 tools
Leave 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.
</content>
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。