Apple Search Ads MCP Server
Provides read-only access to Apple's advertising APIs (Apple Search Ads Campaign Management API v5 and Apple Ads Platform API v1) across multiple accounts, with tools for retrieving campaigns, budget orders, ad groups, targeting and negative keywords, ads, creatives, product pages, assets, geo locations, apps, change history, and user/account ACL details.
Click on "Deploy 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 Search Ads MCP Serverlist all my campaigns across Apple Search Ads accounts"
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 Search Ads MCP Server (read-only)
A production-ready Model Context Protocol server that gives MCP clients (Claude Desktop, Claude Code, Codex, Cursor and others) read-only access to Apple's advertising APIs, for multiple Apple Search Ads accounts, over stdio or Streamable HTTP (Docker-ready).
This server intentionally supports only Apple Search Ads GET/read-only APIs. Mutation APIs are not implemented. Report-generation APIs and other endpoints that Apple only exposes through
POST(including read-style/findand/querycalls) are intentionally excluded too.
What it covers
Apple currently documents two advertising APIs, and this server covers every GET endpoint of both (54 endpoints, verified against Apple's official documentation on 2026-10-03):
API | Base URL | Status | GET endpoints | MCP tool prefix |
Apple Search Ads Campaign Management API v5 |
| Deprecated by Apple, sunset 2027-01-26 | 30 |
|
Apple Ads Platform API v1 (released Aug 2026) |
| Current | 24 |
|
The full inventory, including the 120 intentionally excluded non-GET endpoints, is in
docs/API-COVERAGE.md (machine-readable: docs/api-coverage.json).
Set ENABLED_APIS=platform to drop the v5 tools after Apple's sunset date.
MCP tools (55)
list_accounts, plus one tool per Apple GET endpoint:
Campaign Management API v5 — v5_get_user_acl, v5_get_me_details, v5_search_apps, v5_get_app_details,
v5_get_localized_app_details, v5_get_campaign, v5_get_all_campaigns, v5_get_budget_order,
v5_get_all_budget_orders, v5_get_ad_group, v5_get_all_ad_groups, v5_get_targeting_keyword,
v5_get_all_targeting_keywords, v5_get_campaign_negative_keyword, v5_get_all_campaign_negative_keywords,
v5_get_ad_group_negative_keyword, v5_get_all_ad_group_negative_keywords, v5_search_geolocations, v5_get_ad,
v5_get_all_ads, v5_get_ad_creative_rejection_reason, v5_get_creative, v5_get_all_creatives,
v5_get_product_pages, v5_get_product_page, v5_get_product_page_locales, v5_get_supported_countries_or_regions,
v5_get_app_preview_device_sizes, v5_get_impression_share_report, v5_get_all_impression_share_reports
Apple Ads Platform API v1 — platform_get_me, platform_get_user_acls, platform_get_org,
platform_get_ad_account, platform_get_advertiser_resources, platform_search_apps, platform_get_app_details,
platform_get_app_rejection_reason, platform_get_brand, platform_get_business_category,
platform_get_location_group, platform_get_location, platform_get_campaign,
platform_get_campaign_legacy_app_limited_status_reasons, platform_get_ad_group, platform_search_geo_locations,
platform_get_keyword, platform_get_negative_keyword, platform_get_ad, platform_get_creative,
platform_get_asset, platform_get_product_page, platform_get_budget_order, platform_get_change_history_detail
There is deliberately no generic request(method, url, body) tool. MCP resources apple-ads://accounts and
apple-ads://endpoints describe the configured accounts and the implemented endpoints.
What you can't do with GET only. Apple exposes reports (spend, keyword and search-term reports), impression-share report creation, "find"/"query" listing with filters, and every Platform API list (campaigns, ad groups, keywords, ads, creatives, budget orders, insights, recommendations, suggestions, change-history search) only through POST. Those are listed as excluded in docs/API-COVERAGE.md. With the Platform API you can read any entity whose id you know; with v5 you can also list entities via the GET list endpoints.
Related MCP server: HBCA Meta Ads MCP
Quick start
npm ci
npm run build
cp config/accounts.example.json config/accounts.json # fill in real credentials, then: chmod 600 config/accounts.json
ACCOUNTS_CONFIG=config/accounts.json npm run start:stdioRequires Node.js 22+.
Authentication
Each account uses Apple's OAuth 2.0 client-credentials flow (same for both APIs):
In Apple Ads, invite an API user, generate a P-256 key (
openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem), upload the public key, and note the clientId, teamId and keyId.The server signs an ES256 client-secret JWT (
iss=teamId,sub=clientId,aud=https://appleid.apple.com,kid=keyId, 1-hour lifetime by default; Apple allows up to 180 days), exchanges it atPOST https://appleid.apple.com/auth/oauth2/token(scope=searchadsorg) for a 1-hour access token, caches it per account, refreshes it 60 s before expiry, and refreshes once more if Apple returns 401.Requests carry
Authorization: Bearer …andX-AP-Context: orgId=…(v5) orX-AP-Context: adAccountId=…(Platform API).
The OAuth token exchange is the only non-GET request the server ever makes; it goes to the fixed Apple ID token endpoint and is not reachable from any tool. See docs/SECURITY.md.
Multiple accounts
Accounts live in a JSON file (ACCOUNTS_CONFIG). Every tool takes an account_id:
{
"accounts": [
{
"id": "production",
"name": "Production",
"clientId": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"teamId": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"keyId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"privateKey": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----",
"orgId": "1234567",
"adAccountId": "123456789"
},
{
"id": "staging",
"name": "Staging",
"clientId": "SEARCHADS.…",
"teamId": "SEARCHADS.…",
"keyId": "…",
"privateKeyPath": "../secrets/staging.pem",
"orgId": "7654321",
"apis": ["campaign-management-v5"]
}
]
}Exactly one of
privateKey,privateKeyPath(relative to the config file) orprivateKeyEnv(name of an environment variable) per account.orgId/adAccountIdare optional defaults; tools acceptorg_id/ad_account_idto override them. Discover them withv5_get_user_aclandplatform_get_user_acls.apisoptionally restricts an account to one API family.Each account gets its own isolated token cache; credentials are never returned by any tool or written to logs.
Full reference: docs/CONFIGURATION.md.
Usage examples
list_accounts {}
v5_get_all_campaigns { "account_id": "production" }
v5_get_all_campaigns { "account_id": "production", "fetch_all": true, "max_records": 5000 }
v5_get_ad_group { "account_id": "production", "campaignId": "542370642", "adgroupId": "542317095" }
platform_get_user_acls { "account_id": "production" }
platform_get_campaign { "account_id": "production", "ad_account_id": "123456789", "id": "444555681" }
platform_search_geo_locations { "account_id": "production", "supplySource": "APPSTORE", "query": "San Francisco", "entity": "Locality" }Responses keep Apple's payload unchanged under data and add metadata:
{
"account_id": "production",
"api": "campaign-management-v5",
"tool": "v5_get_all_campaigns",
"data": {
"data": [{ "id": 542370539, "name": "…" }],
"pagination": { "totalResults": 250, "startIndex": 0, "itemsPerPage": 20 },
"error": null
},
"pagination": {
"mode": "single_page",
"offset": 0,
"limit": 20,
"total": 250,
"records_returned": 20,
"has_more": true,
"next_offset": 20
},
"meta": { "request_id": "…" }
}With fetch_all: true, data is the array of records collected across pages (bounded by max_pages /
max_records, default 10 pages / 1,000 records), and pagination.truncated tells you if a limit stopped it.
Errors share one shape:
{
"error": {
"type": "APPLE_SEARCH_ADS_API_ERROR",
"status": 401,
"message": "Authentication failed (401): …",
"request_id": "…"
}
}Types: CONFIGURATION_ERROR, ACCOUNT_NOT_FOUND, AUTHENTICATION_ERROR, APPLE_SEARCH_ADS_API_ERROR,
RATE_LIMIT_ERROR, VALIDATION_ERROR, UNSUPPORTED_OPERATION, NETWORK_ERROR, MALFORMED_RESPONSE,
INTERNAL_ERROR. 429 responses are retried with Retry-After (or RateLimit-Reset), 5xx and transient network
errors with exponential backoff (Apple's 2/4/8/16 s guidance).
Running over stdio
npm run start:stdio # = node dist/index.js --transport stdiostdout carries only MCP messages; all logs are JSON lines on stderr.
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"apple-search-ads": {
"command": "node",
"args": ["/absolute/path/to/apple-search-ads-mcp/dist/index.js", "--transport", "stdio"],
"env": { "ACCOUNTS_CONFIG": "/absolute/path/to/accounts.json" }
}
}
}Claude Code
claude mcp add apple-search-ads -e ACCOUNTS_CONFIG=/absolute/path/to/accounts.json \
-- node /absolute/path/to/apple-search-ads-mcp/dist/index.js --transport stdio
# or, against the HTTP server:
claude mcp add --transport http apple-search-ads http://localhost:8080/mcp --header "Authorization: Bearer $MCP_AUTH_TOKEN"Codex (~/.codex/config.toml)
[mcp_servers.apple-search-ads]
command = "node"
args = ["/absolute/path/to/apple-search-ads-mcp/dist/index.js", "--transport", "stdio"]
env = { ACCOUNTS_CONFIG = "/absolute/path/to/accounts.json" }Cursor / VS Code (.cursor/mcp.json, .vscode/mcp.json)
{
"mcpServers": {
"apple-search-ads": {
"command": "node",
"args": ["/absolute/path/to/apple-search-ads-mcp/dist/index.js", "--transport", "stdio"],
"env": { "ACCOUNTS_CONFIG": "/absolute/path/to/accounts.json" }
}
}
}Remote clients that speak Streamable HTTP use http://<host>:8080/mcp with the header
Authorization: Bearer <MCP_AUTH_TOKEN>.
Running over HTTP
MCP_AUTH_TOKEN=$(openssl rand -hex 32) ACCOUNTS_CONFIG=config/accounts.json PORT=8080 HOST=127.0.0.1 npm run start:http
curl http://127.0.0.1:8080/health # {"status":"ok"}POST /mcp— MCP Streamable HTTP (stateless, JSON responses), bearer-protected whenMCP_AUTH_TOKENis set.GET /health— liveness; never exposes configuration.
Docker
docker build -t apple-search-ads-mcp .
docker run -p 8080:8080 \
-v ./config/accounts.json:/app/config/accounts.json:ro \
-e ACCOUNTS_CONFIG=/app/config/accounts.json \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
apple-search-ads-mcp
# or
MCP_AUTH_TOKEN=$(openssl rand -hex 32) docker compose up -d --buildMulti-stage build, Alpine runtime, non-root node user, HEALTHCHECK on /health, no credentials in the image.
Published images: every push to main that passes CI is pushed to GHCR as
ghcr.io/<owner>/<repo>:latest and :sha-<commit> (linux/amd64 + linux/arm64). npm run docker:publish does the
same from your machine. The image also runs as a stdio server: docker run -i --rm -v …/accounts.json:/app/config/accounts.json:ro ghcr.io/<owner>/<repo>:latest --transport stdio.
Details: docs/DOCKER.md.
Testing
npm run lint && npm run typecheck
npm run test:unit # unit + security: every GET endpoint × 17 scenarios, every tool × 9 scenarios
npm run test:integration # MSW-mocked Apple APIs at their real URLs, through the MCP protocol
npm run test:e2e # builds, then drives the compiled server over stdio and HTTP against a mock Apple API
npm run test:coverage
npm run docker:test # docker build + smoke test (health, non-root, no baked creds, MCP over HTTP)No test ever calls Apple. See docs/TESTING.md.
Configuration (environment)
Variable | Default | Purpose |
|
| Accounts/credentials JSON file |
|
|
|
|
| HTTP bind address |
| – | Bearer token required on |
|
|
|
|
| Per-request timeout (ms) |
| both |
|
All variables: docs/CONFIGURATION.md and .env.example.
Project layout
src/
├── index.ts CLI entry (stdio | http)
├── runtime.ts wires config → accounts → auth → GET-only client → MCP tools
├── apple-search-ads/ Apple client, independent of MCP
│ ├── apis.ts API families (base URLs, context header, envelopes)
│ ├── auth.ts ES256 client secret, OAuth token client, per-account token cache
│ ├── accounts.ts AccountManager (AuthenticationProvider, isolation)
│ ├── http.ts GetOnlyHttpClient (method guard, URL allow-list, timeouts, parsing)
│ ├── client.ts AppleSearchAdsClient (URL building, retries, 401 refresh, error mapping)
│ ├── pagination.ts paginate / paginatePages / collectPages
│ ├── rate-limit.ts, retry.ts, errors.ts
│ └── endpoints/ declarative GET endpoint registry (v5 + Platform)
├── mcp/ server, tools (one per endpoint + list_accounts), schemas, resources
├── transport/ stdio.ts, http.ts
├── config/ env.ts, loader.ts (accounts JSON)
└── utils/ logger.ts, redact.ts, time.ts
tests/{unit,security,integration,e2e,fixtures,helpers}
docs/{API-COVERAGE.md,api-coverage.json,ARCHITECTURE.md,CONFIGURATION.md,DOCKER.md,TESTING.md,SECURITY.md}Adding a newly documented GET endpoint is a registry entry plus an inventory entry and a fixture — see docs/ARCHITECTURE.md.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Read-only MCP access to a documented IT fleet: state, changes, posture. 15 tools.
Read and edit GA4, Search Console and Google Tag Manager from any MCP client. 29 tools.
- SiglataOAuthcom.siglata
One MCP tool per operation over OAuth: read, search and query company files, then save edits.
Related MCP Servers
- AlicenseBqualityBmaintenanceRead-only MCP server for Meta Ads that lists and reads ad accounts, campaigns, ad sets, ads, ad images, creatives, and fetches insights at various levels.14MIT
- FlicenseNot gradedqualityBmaintenanceA read-only MCP server for querying Meta Ads accounts through Meta's Marketing API. It provides tools to retrieve ad accounts, campaigns, ad sets, ads, and performance insights.-
- AlicenseNot gradedqualityAmaintenanceProvides MCP clients typed tools for Apple Ads Platform research, reporting, auditing, and receipt-gated campaign management with a local-first, read-only-by-default safety model.5MIT
- FlicenseAqualityBmaintenanceProvides read-only MCP tools over stdio for interacting with a Redmine instance, enabling users to list and retrieve projects, issues, queries, wiki pages, and other Redmine resources via the REST API.35-