@nerdsnipe-inc/apple-ads-mcp-server 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +534 -2
  5. package/bin/apple-ads-mcp-server.js +2 -0
  6. package/dist/client.d.ts +100 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +352 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/config.d.ts +47 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +134 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/errors.d.ts +24 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +96 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/generated/schemas.d.ts +8434 -0
  19. package/dist/generated/schemas.d.ts.map +1 -0
  20. package/dist/generated/schemas.js +477 -0
  21. package/dist/generated/schemas.js.map +1 -0
  22. package/dist/index.d.ts +18 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +72 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/redact.d.ts +6 -0
  27. package/dist/redact.d.ts.map +1 -0
  28. package/dist/redact.js +32 -0
  29. package/dist/redact.js.map +1 -0
  30. package/dist/registry.d.ts +12 -0
  31. package/dist/registry.d.ts.map +1 -0
  32. package/dist/registry.js +59 -0
  33. package/dist/registry.js.map +1 -0
  34. package/dist/server.d.ts +23 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +54 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/tooling.d.ts +55 -0
  39. package/dist/tooling.d.ts.map +1 -0
  40. package/dist/tooling.js +146 -0
  41. package/dist/tooling.js.map +1 -0
  42. package/dist/tools/accounts.d.ts +2 -0
  43. package/dist/tools/accounts.d.ts.map +1 -0
  44. package/dist/tools/accounts.js +107 -0
  45. package/dist/tools/accounts.js.map +1 -0
  46. package/dist/tools/ad_groups.d.ts +2 -0
  47. package/dist/tools/ad_groups.d.ts.map +1 -0
  48. package/dist/tools/ad_groups.js +87 -0
  49. package/dist/tools/ad_groups.js.map +1 -0
  50. package/dist/tools/ads.d.ts +2 -0
  51. package/dist/tools/ads.d.ts.map +1 -0
  52. package/dist/tools/ads.js +87 -0
  53. package/dist/tools/ads.js.map +1 -0
  54. package/dist/tools/apps.d.ts +2 -0
  55. package/dist/tools/apps.d.ts.map +1 -0
  56. package/dist/tools/apps.js +61 -0
  57. package/dist/tools/apps.js.map +1 -0
  58. package/dist/tools/assets.d.ts +2 -0
  59. package/dist/tools/assets.d.ts.map +1 -0
  60. package/dist/tools/assets.js +76 -0
  61. package/dist/tools/assets.js.map +1 -0
  62. package/dist/tools/brands.d.ts +2 -0
  63. package/dist/tools/brands.d.ts.map +1 -0
  64. package/dist/tools/brands.js +122 -0
  65. package/dist/tools/brands.js.map +1 -0
  66. package/dist/tools/budget_orders.d.ts +2 -0
  67. package/dist/tools/budget_orders.d.ts.map +1 -0
  68. package/dist/tools/budget_orders.js +87 -0
  69. package/dist/tools/budget_orders.js.map +1 -0
  70. package/dist/tools/bulk.d.ts +2 -0
  71. package/dist/tools/bulk.d.ts.map +1 -0
  72. package/dist/tools/bulk.js +65 -0
  73. package/dist/tools/bulk.js.map +1 -0
  74. package/dist/tools/campaigns.d.ts +2 -0
  75. package/dist/tools/campaigns.d.ts.map +1 -0
  76. package/dist/tools/campaigns.js +102 -0
  77. package/dist/tools/campaigns.js.map +1 -0
  78. package/dist/tools/change_history.d.ts +2 -0
  79. package/dist/tools/change_history.d.ts.map +1 -0
  80. package/dist/tools/change_history.js +44 -0
  81. package/dist/tools/change_history.js.map +1 -0
  82. package/dist/tools/creatives.d.ts +2 -0
  83. package/dist/tools/creatives.d.ts.map +1 -0
  84. package/dist/tools/creatives.js +87 -0
  85. package/dist/tools/creatives.js.map +1 -0
  86. package/dist/tools/eligibility.d.ts +2 -0
  87. package/dist/tools/eligibility.d.ts.map +1 -0
  88. package/dist/tools/eligibility.js +58 -0
  89. package/dist/tools/eligibility.js.map +1 -0
  90. package/dist/tools/geo.d.ts +2 -0
  91. package/dist/tools/geo.d.ts.map +1 -0
  92. package/dist/tools/geo.js +47 -0
  93. package/dist/tools/geo.js.map +1 -0
  94. package/dist/tools/index.d.ts +4 -0
  95. package/dist/tools/index.d.ts.map +1 -0
  96. package/dist/tools/index.js +48 -0
  97. package/dist/tools/index.js.map +1 -0
  98. package/dist/tools/insights.d.ts +2 -0
  99. package/dist/tools/insights.d.ts.map +1 -0
  100. package/dist/tools/insights.js +46 -0
  101. package/dist/tools/insights.js.map +1 -0
  102. package/dist/tools/keywords.d.ts +2 -0
  103. package/dist/tools/keywords.d.ts.map +1 -0
  104. package/dist/tools/keywords.js +87 -0
  105. package/dist/tools/keywords.js.map +1 -0
  106. package/dist/tools/location_groups.d.ts +2 -0
  107. package/dist/tools/location_groups.d.ts.map +1 -0
  108. package/dist/tools/location_groups.js +87 -0
  109. package/dist/tools/location_groups.js.map +1 -0
  110. package/dist/tools/meta.d.ts +7 -0
  111. package/dist/tools/meta.d.ts.map +1 -0
  112. package/dist/tools/meta.js +158 -0
  113. package/dist/tools/meta.js.map +1 -0
  114. package/dist/tools/negative_keywords.d.ts +2 -0
  115. package/dist/tools/negative_keywords.d.ts.map +1 -0
  116. package/dist/tools/negative_keywords.js +87 -0
  117. package/dist/tools/negative_keywords.js.map +1 -0
  118. package/dist/tools/product_pages.d.ts +2 -0
  119. package/dist/tools/product_pages.d.ts.map +1 -0
  120. package/dist/tools/product_pages.js +76 -0
  121. package/dist/tools/product_pages.js.map +1 -0
  122. package/dist/tools/recommendations.d.ts +2 -0
  123. package/dist/tools/recommendations.d.ts.map +1 -0
  124. package/dist/tools/recommendations.js +103 -0
  125. package/dist/tools/recommendations.js.map +1 -0
  126. package/dist/tools/reports_apps.d.ts +2 -0
  127. package/dist/tools/reports_apps.d.ts.map +1 -0
  128. package/dist/tools/reports_apps.js +114 -0
  129. package/dist/tools/reports_apps.js.map +1 -0
  130. package/dist/tools/reports_brands.d.ts +2 -0
  131. package/dist/tools/reports_brands.d.ts.map +1 -0
  132. package/dist/tools/reports_brands.js +114 -0
  133. package/dist/tools/reports_brands.js.map +1 -0
  134. package/dist/tools/suggestions.d.ts +2 -0
  135. package/dist/tools/suggestions.d.ts.map +1 -0
  136. package/dist/tools/suggestions.js +77 -0
  137. package/dist/tools/suggestions.js.map +1 -0
  138. package/dist/types.d.ts +43 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +2 -0
  141. package/dist/types.js.map +1 -0
  142. package/dist/version.d.ts +4 -0
  143. package/dist/version.d.ts.map +1 -0
  144. package/dist/version.js +26 -0
  145. package/dist/version.js.map +1 -0
  146. package/docs/COVERAGE.md +144 -0
  147. package/docs/DECISIONS.md +16 -0
  148. package/docs/coverage.json +1028 -0
  149. package/package.json +76 -4
package/.env.example ADDED
@@ -0,0 +1,20 @@
1
+ # Apple Ads Platform API credentials (placeholders only - never commit real values)
2
+ # Create them in Apple Ads > Account Settings > API (see README).
3
+ APPLE_ADS_CLIENT_ID=SEARCHADS.00000000-0000-0000-0000-000000000000
4
+ APPLE_ADS_TEAM_ID=SEARCHADS.00000000-0000-0000-0000-000000000000
5
+ APPLE_ADS_KEY_ID=00000000-0000-0000-0000-000000000000
6
+ # Path to the EC (prime256v1) private key; "~" is expanded.
7
+ APPLE_ADS_PRIVATE_KEY_PATH=~/.appleads/private_keys/private-key.pem
8
+ # Alternative to the path: the PEM contents inline (use \n for newlines).
9
+ # APPLE_ADS_PRIVATE_KEY=
10
+
11
+ # Optional defaults
12
+ # Default ad account ID sent as X-AP-Context: adAccountId=<id>
13
+ # APPLE_ADS_AD_ACCOUNT_ID=
14
+ # Default org ID for org-level calls (also used to resolve the ad account)
15
+ # APPLE_ADS_ORG_ID=
16
+
17
+ # Safety switches
18
+ # APPLE_ADS_READ_ONLY=true
19
+ # APPLE_ADS_ENABLED_GROUPS=accounts,campaigns,reports_apps
20
+ # APPLE_ADS_DISABLED_GROUPS=budget_orders
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/) and the project uses [SemVer](https://semver.org/).
5
+
6
+ ## [1.0.0] - 2026-10-10
7
+
8
+ ### Added
9
+ - Initial release: 99 tools, one per operation of `@apple/apple-ads-platform` 1.109.0 (Apple Ads Platform API v1), covering 22 groups.
10
+ - Meta tools: `apple_ads_request` (escape hatch), `apple_ads_list_operations`, `apple_ads_server_status`.
11
+ - Key-based OAuth (ES256 client secret generated by the SDK), token caching, refresh-and-retry on 401, Retry-After aware backoff on 429/5xx.
12
+ - Per-call `adAccountId` context override plus `APPLE_ADS_AD_ACCOUNT_ID` / `APPLE_ADS_ORG_ID` defaults.
13
+ - Safety switches: `APPLE_ADS_READ_ONLY`, `APPLE_ADS_ENABLED_GROUPS`, `APPLE_ADS_DISABLED_GROUPS`.
14
+ - Generator (`scripts/generate.mjs`) with a drift gate, `docs/COVERAGE.md` manifest and generated coverage test.
15
+ - `scripts/install-global.sh` for Claude Code and Claude Desktop.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nerdsnipe Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,535 @@
1
- # Temporary Holding Version
1
+ # Apple Ads MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@nerdsnipe-inc/apple-ads-mcp-server)](https://www.npmjs.com/package/@nerdsnipe-inc/apple-ads-mcp-server)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@nerdsnipe-inc/apple-ads-mcp-server)](https://www.npmjs.com/package/@nerdsnipe-inc/apple-ads-mcp-server)
5
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
7
+ [![MCP](https://img.shields.io/badge/protocol-MCP-purple)](https://modelcontextprotocol.io)
8
+
9
+ A Model Context Protocol (MCP) server that gives AI assistants (Claude Code, Claude Desktop, Cursor, Windsurf, ...) full access to the **Apple Ads Platform API (v1)**, built on the official [`@apple/apple-ads-platform`](https://www.npmjs.com/package/@apple/apple-ads-platform) SDK.
10
+
11
+ > This targets the **Apple Ads Platform API** (`https://api.ads.apple.com/v1`), not the older Campaign Management API v5.
12
+
13
+ - One tool per SDK operation, 100% coverage (see [docs/COVERAGE.md](docs/COVERAGE.md))
14
+ - Escape hatch tool (`apple_ads_request`) for any endpoint Apple adds before the SDK does
15
+ - Automatic OAuth (ES256 client secret, token caching, refresh on 401), Retry-After aware retries on 429/5xx
16
+ - Safety switches: read-only mode and tool-group allow/deny lists
17
+ - Starts and lists tools even without credentials (calls then return an actionable error)
18
+
19
+ ## Quick start (no local install)
20
+
21
+ There is nothing to clone or build. Your AI client launches the server on demand with `npx`, which downloads the published package the first time and caches it. You only need [Node.js 20+](https://nodejs.org) and your Apple Ads API credentials (see [Prerequisites](#prerequisites)).
22
+
23
+ **Claude Code** (all projects):
24
+
25
+ ```bash
26
+ claude mcp add apple-ads --scope user \
27
+ -e APPLE_ADS_CLIENT_ID=SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
28
+ -e APPLE_ADS_TEAM_ID=SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
29
+ -e APPLE_ADS_KEY_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
30
+ -e APPLE_ADS_PRIVATE_KEY_PATH=~/.appleads/private_keys/private-key.pem \
31
+ -- npx -y @nerdsnipe-inc/apple-ads-mcp-server
32
+ ```
33
+
34
+ **Any other MCP client** uses the same block in its config file:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "apple-ads": {
40
+ "command": "npx",
41
+ "args": ["-y", "@nerdsnipe-inc/apple-ads-mcp-server"],
42
+ "env": {
43
+ "APPLE_ADS_CLIENT_ID": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
44
+ "APPLE_ADS_TEAM_ID": "SEARCHADS.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
45
+ "APPLE_ADS_KEY_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
46
+ "APPLE_ADS_PRIVATE_KEY_PATH": "/Users/you/.appleads/private_keys/private-key.pem"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ Per-client details are in [Installation](#installation). To verify the package without credentials, run `npx -y @nerdsnipe-inc/apple-ads-mcp-server --list-tools`.
54
+
55
+ ## Coverage
56
+
57
+ <!-- GENERATED:TOOLS:BEGIN -->
58
+ **102 tools** = 99 Apple Ads Platform API operations (100% of `@apple/apple-ads-platform`) + 3 meta tools.
59
+
60
+ | Group | Tools | Covers |
61
+ |---|---:|---|
62
+ | `accounts` | 7 | Org, user identity (me/ACLs), ad accounts and advertiser resources |
63
+ | `apps` | 3 | App Store app search, app details and supported ad languages |
64
+ | `eligibility` | 3 | App eligibility checks and App Store creative rejection reasons |
65
+ | `brands` | 7 | Apple Maps brands, business categories, brand rejection reasons and locations |
66
+ | `location_groups` | 5 | Apple Maps location groups (create, query, get, update, delete) |
67
+ | `campaigns` | 6 | Campaigns (App Store and Apple Maps) |
68
+ | `ad_groups` | 5 | Ad groups (targeting, bid strategy, schedule) |
69
+ | `ads` | 5 | Ads (link a creative to an ad group) |
70
+ | `creatives` | 5 | Ad creatives (product page and Apple Maps creatives) |
71
+ | `assets` | 4 | Apple Maps creative assets (upload, query, get, delete) |
72
+ | `keywords` | 5 | Keywords |
73
+ | `negative_keywords` | 5 | Negative keywords |
74
+ | `bulk` | 4 | Bulk create/update of keywords and negative keywords |
75
+ | `geo` | 2 | Geo targeting lookups |
76
+ | `product_pages` | 4 | App Store product pages and locale details |
77
+ | `budget_orders` | 5 | Budget orders (shared budgets) for monthly-invoiced accounts |
78
+ | `reports_apps` | 5 | App Store performance reports |
79
+ | `reports_brands` | 5 | Apple Maps (business brand) performance reports |
80
+ | `insights` | 2 | Impression share and search term popularity |
81
+ | `recommendations` | 6 | Daily budget and target CPA recommendations (query, apply, dismiss) |
82
+ | `suggestions` | 4 | Keyword, phrase, category and target CPA suggestions |
83
+ | `change_history` | 2 | Audit log of changes (summaries and field-level details) |
84
+ | `meta` | 3 | `apple_ads_request` (raw escape hatch), `apple_ads_list_operations`, `apple_ads_server_status` |
85
+ | **Total** | **102** | |
86
+
87
+ ### All tools
88
+
89
+ #### accounts
90
+
91
+ | Tool | Method | Path | Description |
92
+ |---|---|---|---|
93
+ | `apple_ads_get_ad_account` | GET | `/v1/ad-accounts/{id}` | Get full details of an ad account (currency, timezone, payment model, status, product features, delegations). Defaults to the adAccountId in context. |
94
+ | `apple_ads_update_ad_account` | PUT | `/v1/ad-accounts/{id}` | Update an ad account's name or delegations. Array fields use full-replacement semantics, so send the complete desired array. |
95
+ | `apple_ads_create_ad_account` | POST | `/v1/ad-accounts` | Create a new ad account under the caller's org, optionally delegating an App Store content provider or Apple Maps brand to it. Needs no ad account context. |
96
+ | `apple_ads_get_advertiser_resources` | GET | `/v1/advertiser-resources` | List advertiser resources (App Store content providers or Apple Maps business brands) available in the org that can be delegated to an ad account. Needs no ad account context. |
97
+ | `apple_ads_get_me` | GET | `/v1/me` | Get the authenticated API user's userId and orgId. Needs no ad account context, so it is the best first call to verify credentials. |
98
+ | `apple_ads_get_user_acls` | GET | `/v1/acls` | List every ad account this API user can access, with the user's roles on each. Use it to discover adAccountId values (needs no ad account context). |
99
+ | `apple_ads_get_org` | GET | `/v1/orgs/{id}` | Get an organization's name, currency, timezone, payment model and system status. Defaults to the configured/authenticated org when orgId is omitted. |
100
+
101
+ #### apps
102
+
103
+ | Tool | Method | Path | Description |
104
+ |---|---|---|---|
105
+ | `apple_ads_get_app_details` | GET | `/v1/apps/{adamId}` | Get details for one app by its Adam ID (name, genres, device classes, available storefronts). |
106
+ | `apple_ads_query_supported_app_languages` | POST | `/v1/metadata/apps/supported-languages/query` | Query the languages supported for App Store advertising in each country or region. |
107
+ | `apple_ads_search_apps` | GET | `/v1/search/apps` | Search the App Store for apps by name/developer, by content provider IDs, or list the apps owned by your org. Use it to find the adamId used as a campaign's promotedObjectId. |
108
+
109
+ #### eligibility
110
+
111
+ | Tool | Method | Path | Description |
112
+ |---|---|---|---|
113
+ | `apple_ads_check_app_eligibility` | POST | `/v1/eligibilities/apps/query` | Check whether an app is eligible to advertise, per placement and country or region. |
114
+ | `apple_ads_query_app_rejection_reasons` | POST | `/v1/rejection-reasons/apps/query` | Query human-readable rejection reasons for App Store ad creatives of an app. |
115
+ | `apple_ads_get_app_rejection_reason` | GET | `/v1/rejection-reasons/apps/{rejectionReasonId}` | Get one App Store creative rejection reason by its ID. |
116
+
117
+ #### brands
118
+
119
+ | Tool | Method | Path | Description |
120
+ |---|---|---|---|
121
+ | `apple_ads_get_brand` | GET | `/v1/business-brands/{id}` | Get one Apple Maps business brand by ID. |
122
+ | `apple_ads_get_business_category` | GET | `/v1/business-categories/{id}` | Get one Apple Maps business category by ID. |
123
+ | `apple_ads_get_location` | GET | `/v1/locations/{id}` | Get one Apple Maps business location by ID. |
124
+ | `apple_ads_query_brands` | POST | `/v1/business-brands/query` | Query Apple Maps business brands. Filter by eligibility.status ELIGIBLE to find brands usable in campaigns. |
125
+ | `apple_ads_query_business_categories` | POST | `/v1/business-categories/query` | Query Apple Maps business categories. |
126
+ | `apple_ads_query_locations` | POST | `/v1/locations/query` | Query the business locations of your Apple Maps brands (read-only; filter by brandId and status OPEN to build location groups). |
127
+ | `apple_ads_query_brand_rejection_reasons` | POST | `/v1/rejection-reasons/business-brands/query` | Query policy assignments (rejection reasons) for Apple Maps brands and creatives, filtered by the brand's promotedObjectId. |
128
+
129
+ #### location_groups
130
+
131
+ | Tool | Method | Path | Description |
132
+ |---|---|---|---|
133
+ | `apple_ads_create_location_group` | POST | `/v1/location-groups` | Create an Apple Maps location group (STATIC list of location IDs or DYNAMIC rule-based) for ad group targeting. |
134
+ | `apple_ads_delete_location_group` | DELETE | `/v1/location-groups/{id}` | Permanently delete a location group. Ad groups targeting it lose that targeting constraint immediately. |
135
+ | `apple_ads_get_location_group` | GET | `/v1/location-groups/{id}` | Get one location group by ID. |
136
+ | `apple_ads_query_location_groups` | POST | `/v1/location-groups/query` | Query location groups with filters, sorting and pagination. |
137
+ | `apple_ads_update_location_group` | PUT | `/v1/location-groups/{id}` | Update a location group (name, description, location IDs or rules). INVALID/PENDING groups cannot be updated. |
138
+
139
+ #### campaigns
140
+
141
+ | Tool | Method | Path | Description |
142
+ |---|---|---|---|
143
+ | `apple_ads_delete_campaign` | DELETE | `/v1/campaigns/{id}` | Soft-delete a campaign by ID. |
144
+ | `apple_ads_get_campaign` | GET | `/v1/campaigns/{id}` | Get one campaign by ID, including status, systemStatus, bid strategy and budget. |
145
+ | `apple_ads_get_campaign_legacy_limited_status_details` | GET | `/v1/campaigns/{id}/legacy-app-limited-status-reason-details` | Get per-country limited-status reason details for a legacy app campaign. |
146
+ | `apple_ads_update_campaign` | PUT | `/v1/campaigns/{id}` | Partially update a campaign (name, status ENABLED/PAUSED, budget, bid strategy, targeting). Only send fields to change; arrays are fully replaced. |
147
+ | `apple_ads_create_campaign` | POST | `/v1/campaigns` | Create a campaign for an App Store app (APPSTORE_APP) or Apple Maps brand (BUSINESS_BRAND). promotedObjectType and promotedObjectId are immutable after creation. |
148
+ | `apple_ads_query_campaigns` | POST | `/v1/campaigns/query` | Query campaigns with filters, sorting and pagination. With no filters, returns all non-deleted campaigns in the ad account. |
149
+
150
+ #### ad_groups
151
+
152
+ | Tool | Method | Path | Description |
153
+ |---|---|---|---|
154
+ | `apple_ads_delete_ad_group` | DELETE | `/v1/adgroups/{id}` | Soft-delete an ad group by ID. |
155
+ | `apple_ads_get_ad_group` | GET | `/v1/adgroups/{id}` | Get one ad group by ID. |
156
+ | `apple_ads_update_ad_group` | PUT | `/v1/adgroups/{id}` | Partially update an ad group (status, bids, targeting, schedule). Arrays are fully replaced. |
157
+ | `apple_ads_create_ad_group` | POST | `/v1/adgroups` | Create an ad group inside a campaign (targeting, pricing model, bid strategy, schedule, Search Match). |
158
+ | `apple_ads_query_ad_groups` | POST | `/v1/adgroups/query` | Query ad groups with filters (e.g. campaignId), sorting and pagination. |
159
+
160
+ #### ads
161
+
162
+ | Tool | Method | Path | Description |
163
+ |---|---|---|---|
164
+ | `apple_ads_delete_ad` | DELETE | `/v1/ads/{id}` | Soft-delete an ad by ID. |
165
+ | `apple_ads_get_ad` | GET | `/v1/ads/{id}` | Get one ad by ID. |
166
+ | `apple_ads_update_ad` | PUT | `/v1/ads/{id}` | Update an ad. Only name and status (ENABLED/PAUSED) can change after creation. |
167
+ | `apple_ads_create_ad` | POST | `/v1/ads` | Create an ad linking an existing creative to an ad group. Create the creative first. |
168
+ | `apple_ads_query_ads` | POST | `/v1/ads/query` | Query ads with filters, sorting and pagination. |
169
+
170
+ #### creatives
171
+
172
+ | Tool | Method | Path | Description |
173
+ |---|---|---|---|
174
+ | `apple_ads_delete_creative` | DELETE | `/v1/creatives/{id}` | Soft-delete an ad creative by ID. |
175
+ | `apple_ads_get_creative` | GET | `/v1/creatives/{id}` | Get one ad creative by ID. |
176
+ | `apple_ads_update_creative` | PUT | `/v1/creatives/{id}` | Update an ad creative. |
177
+ | `apple_ads_create_creative` | POST | `/v1/creatives` | Create an ad creative (DEFAULT_PRODUCT_PAGE, CUSTOM_PRODUCT_PAGE or LOCAL_ADS_SEARCH_CREATIVE). creativeType cannot change later. |
178
+ | `apple_ads_query_creatives` | POST | `/v1/creatives/query` | Query ad creatives with filters (creativeType, systemStatus, name, eligibility.status), sorting and pagination. |
179
+
180
+ #### assets
181
+
182
+ | Tool | Method | Path | Description |
183
+ |---|---|---|---|
184
+ | `apple_ads_delete_asset` | DELETE | `/v1/assets/{id}` | Soft-delete an asset you uploaded. |
185
+ | `apple_ads_get_asset` | GET | `/v1/assets/{id}` | Get one asset by ID, including eligibility.status. Poll it after upload until ELIGIBLE. |
186
+ | `apple_ads_query_assets` | POST | `/v1/assets/query` | Query creative assets (App Store apps and Apple Maps brands) with filters, sorting and pagination. |
187
+ | `apple_ads_upload_asset` | POST | `/v1/assets/upload` | Upload an image asset (PNG, JPG or HEIC) for an Apple Maps brand. Provide a local filePath or base64 content. |
188
+
189
+ #### keywords
190
+
191
+ | Tool | Method | Path | Description |
192
+ |---|---|---|---|
193
+ | `apple_ads_delete_keyword` | DELETE | `/v1/keywords/{id}` | Delete a keyword by ID. |
194
+ | `apple_ads_get_keyword` | GET | `/v1/keywords/{id}` | Get one keyword by ID. |
195
+ | `apple_ads_update_keyword` | PUT | `/v1/keywords/{id}` | Update a keyword (bid, status). |
196
+ | `apple_ads_create_keyword` | POST | `/v1/keywords` | Create a keyword in an ad group. |
197
+ | `apple_ads_query_keywords` | POST | `/v1/keywords/query` | Query keywords with filters (e.g. adGroupId), sorting and pagination. |
198
+
199
+ #### negative_keywords
200
+
201
+ | Tool | Method | Path | Description |
202
+ |---|---|---|---|
203
+ | `apple_ads_delete_negative_keyword` | DELETE | `/v1/negative-keywords/{id}` | Delete a negative keyword by ID. |
204
+ | `apple_ads_get_negative_keyword` | GET | `/v1/negative-keywords/{id}` | Get one negative keyword by ID. |
205
+ | `apple_ads_update_negative_keyword` | PUT | `/v1/negative-keywords/{id}` | Update a negative keyword. |
206
+ | `apple_ads_create_negative_keyword` | POST | `/v1/negative-keywords` | Create a negative keyword at campaign or ad group level. |
207
+ | `apple_ads_query_negative_keywords` | POST | `/v1/negative-keywords/query` | Query negative keywords with filters, sorting and pagination. |
208
+
209
+ #### bulk
210
+
211
+ | Tool | Method | Path | Description |
212
+ |---|---|---|---|
213
+ | `apple_ads_bulk_create_keywords` | POST | `/v1/keywords/bulk-create` | Create many keywords in one request (counts as one call against the rate limit). Set allowPartialSuccess to keep valid items when others fail. |
214
+ | `apple_ads_bulk_update_keywords` | POST | `/v1/keywords/bulk-update` | Update many keywords in one request. Each item's data must include the keyword id plus only the fields to change. |
215
+ | `apple_ads_bulk_create_negative_keywords` | POST | `/v1/negative-keywords/bulk-create` | Create many negative keywords in one request. |
216
+ | `apple_ads_bulk_update_negative_keywords` | POST | `/v1/negative-keywords/bulk-update` | Update many negative keywords in one request. Each item's data must include the id plus only the fields to change. |
217
+
218
+ #### geo
219
+
220
+ | Tool | Method | Path | Description |
221
+ |---|---|---|---|
222
+ | `apple_ads_get_geos_by_ids` | POST | `/v1/search/geo` | Look up geo location metadata by entity ID or legacy ID (POST /search/geo). |
223
+ | `apple_ads_search_geos` | GET | `/v1/search/geo` | Search geo locations (country, admin area, locality, postal code) by name or wildcard to find IDs for ad group geo targeting. |
224
+
225
+ #### product_pages
226
+
227
+ | Tool | Method | Path | Description |
228
+ |---|---|---|---|
229
+ | `apple_ads_get_product_page` | GET | `/v1/product-pages/{productPageId}` | Get one product page by its productPageId. |
230
+ | `apple_ads_query_app_locale_details` | POST | `/v1/apps/{adamId}/locale-details/query` | Query the default product page's localized metadata for an app by Adam ID. |
231
+ | `apple_ads_query_product_page_locale_details` | POST | `/v1/product-pages/locale-details/query` | Query localized metadata (name, subtitle, promo text, screenshots) of product pages. |
232
+ | `apple_ads_query_product_pages` | POST | `/v1/product-pages/query` | Query App Store product pages (default, custom and optimized) available to the ad account. |
233
+
234
+ #### budget_orders
235
+
236
+ | Tool | Method | Path | Description |
237
+ |---|---|---|---|
238
+ | `apple_ads_delete_budget_order` | DELETE | `/v1/shared-budgets/{id}` | Delete a budget order by ID. |
239
+ | `apple_ads_get_budget_order` | GET | `/v1/shared-budgets/{id}` | Get one budget order by ID. |
240
+ | `apple_ads_update_budget_order` | PUT | `/v1/shared-budgets/{id}` | Update a budget order. |
241
+ | `apple_ads_create_budget_order` | POST | `/v1/shared-budgets` | Create a budget order (shared budget) capping total spend across campaigns. Requires a monthly-invoiced (LOC) ad account. |
242
+ | `apple_ads_query_budget_orders` | POST | `/v1/shared-budgets/query` | Query budget orders with filters, sorting and pagination. |
243
+
244
+ #### reports_apps
245
+
246
+ | Tool | Method | Path | Description |
247
+ |---|---|---|---|
248
+ | `apple_ads_report_apps_ad_groups` | POST | `/v1/reports/apps/adgroups/query` | App Store ad group performance report for a time range. |
249
+ | `apple_ads_report_apps_ads` | POST | `/v1/reports/apps/ads/query` | App Store ad performance report for a time range. |
250
+ | `apple_ads_report_apps_campaigns` | POST | `/v1/reports/apps/campaigns/query` | App Store campaign performance report (impressions, taps, installs, spend, ...) for a time range, with optional groupBy. |
251
+ | `apple_ads_report_apps_keywords` | POST | `/v1/reports/apps/keywords/query` | App Store keyword performance report for a time range. |
252
+ | `apple_ads_report_apps_search_terms` | POST | `/v1/reports/apps/searchterms/query` | App Store search term performance report for a time range. |
253
+
254
+ #### reports_brands
255
+
256
+ | Tool | Method | Path | Description |
257
+ |---|---|---|---|
258
+ | `apple_ads_report_brands_ad_groups` | POST | `/v1/reports/business-brands/adgroups/query` | Apple Maps ad group performance report for a time range. |
259
+ | `apple_ads_report_brands_ads` | POST | `/v1/reports/business-brands/ads/query` | Apple Maps ad performance report for a time range. |
260
+ | `apple_ads_report_brands_campaigns` | POST | `/v1/reports/business-brands/campaigns/query` | Apple Maps campaign performance report for a time range (group by locationId for per-location breakdowns). |
261
+ | `apple_ads_report_brands_keywords` | POST | `/v1/reports/business-brands/keywords/query` | Apple Maps keyword performance report for a time range. |
262
+ | `apple_ads_report_brands_search_terms` | POST | `/v1/reports/business-brands/searchterms/query` | Apple Maps search term performance report for a time range. |
263
+
264
+ #### insights
265
+
266
+ | Tool | Method | Path | Description |
267
+ |---|---|---|---|
268
+ | `apple_ads_query_impression_share` | POST | `/v1/insights/apps/impression-share/query` | Query App Store impression share for keywords over a time range. A promotedObjectId filter is required. |
269
+ | `apple_ads_query_search_term_popularity` | POST | `/v1/insights/apps/search-term-popularity/query` | Query App Store search term popularity rankings by country or region and genre. |
270
+
271
+ #### recommendations
272
+
273
+ | Tool | Method | Path | Description |
274
+ |---|---|---|---|
275
+ | `apple_ads_apply_daily_budget_recommendations` | POST | `/v1/recommendations/daily-budgets/apply` | Apply daily budget recommendations, which updates the campaigns' daily budgets. All items must share one promotedObjectId. |
276
+ | `apple_ads_apply_target_cpa_recommendations` | POST | `/v1/recommendations/target-cpas/apply` | Apply target CPA recommendations, updating the target CPA used by Maximize Conversions bidding. |
277
+ | `apple_ads_dismiss_daily_budget_recommendations` | POST | `/v1/recommendations/daily-budgets/dismiss` | Dismiss daily budget recommendations without changing any campaign. |
278
+ | `apple_ads_dismiss_target_cpa_recommendations` | POST | `/v1/recommendations/target-cpas/dismiss` | Dismiss target CPA recommendations without changing any campaign. |
279
+ | `apple_ads_query_daily_budget_recommendations` | POST | `/v1/recommendations/daily-budgets/query` | Query daily budget recommendations. promotedObjectId and promotedObjectType filters are mandatory. |
280
+ | `apple_ads_query_target_cpa_recommendations` | POST | `/v1/recommendations/target-cpas/query` | Query target CPA recommendations for App Store campaigns using Maximize Conversions. |
281
+
282
+ #### suggestions
283
+
284
+ | Tool | Method | Path | Description |
285
+ |---|---|---|---|
286
+ | `apple_ads_query_category_suggestions` | POST | `/v1/suggestions/categories/query` | Get category suggestions for an app or brand, or search categories by name. |
287
+ | `apple_ads_query_keyword_suggestions` | POST | `/v1/suggestions/keywords/query` | Get ranked keyword suggestions for an app (popularity 0-100). Feed good ones into apple_ads_create_keyword. |
288
+ | `apple_ads_query_phrase_suggestions` | POST | `/v1/suggestions/phrases/query` | Get natural-language search phrase suggestions for an app or brand, or look up phrase popularity. |
289
+ | `apple_ads_query_target_cpa_suggestion` | POST | `/v1/suggestions/target-cpas/query` | Get a suggested target CPA for an App Store app based on recent tap-install CPI data. |
290
+
291
+ #### change_history
292
+
293
+ | Tool | Method | Path | Description |
294
+ |---|---|---|---|
295
+ | `apple_ads_get_change_details` | GET | `/v1/change-history/{detailId}` | Get field-level before/after values for one change, by detailId (format EntityType.entityId.txnId). |
296
+ | `apple_ads_query_change_history` | POST | `/v1/change-history/query` | Query the change history (audit) summary grouped by transaction. An eventTime filter is required; set options.metadata to latest or snapshot to get detailIds. |
297
+
298
+ #### meta
299
+
300
+ | Tool | Description |
301
+ |---|---|
302
+ | `apple_ads_request` | Raw authenticated request to any Platform API path (forward-compat escape hatch) |
303
+ | `apple_ads_list_operations` | Lists every SDK operation, its HTTP route and the MCP tool that covers it |
304
+ | `apple_ads_server_status` | Redacted configuration/credential diagnostics and rate-limit state |
305
+ <!-- GENERATED:TOOLS:END -->
306
+
307
+ ## Prerequisites
308
+
309
+ 1. An Apple Ads account with API access (Account Settings > API) and Node.js 20+.
310
+ 2. Generate an EC key pair:
311
+
312
+ ```bash
313
+ openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
314
+ openssl ec -in private-key.pem -pubout -out public-key.pem
315
+ ```
316
+
317
+ 3. In Apple Ads go to **Account Settings > API**, paste the contents of `public-key.pem`, and save.
318
+ 4. Copy the three values shown there: **clientId**, **teamId** and **keyId**.
319
+ 5. Keep `private-key.pem` secret (for example in `~/.appleads/private_keys/`, `chmod 600`).
320
+
321
+ ## Installation
322
+
323
+ All clients launch the same command, `npx -y @nerdsnipe-inc/apple-ads-mcp-server`, with the same environment variables (see [Environment variables](#environment-variables)). Only the config file differs. After editing a config, restart the client.
324
+
325
+ > **GUI apps and PATH.** Desktop apps (Claude Desktop, Cursor, Windsurf, VS Code) often do not inherit your shell `PATH`. If you see "spawn npx ENOENT", replace `"npx"` with the absolute path from `which npx` (for example `/opt/homebrew/bin/npx`).
326
+
327
+ > **No key file?** Instead of `APPLE_ADS_PRIVATE_KEY_PATH` you can set `APPLE_ADS_PRIVATE_KEY` to the PEM text itself (newlines written as `\n`). The key then lives only in the client's config. Treat that config file as a secret.
328
+
329
+ ### Claude Code
330
+
331
+ User scope (every project), as shown in [Quick start](#quick-start-no-local-install). To share the server with a team through a project's `.mcp.json`, use `--scope project` and keep the secrets out of git by referencing environment variables:
332
+
333
+ ```json
334
+ {
335
+ "mcpServers": {
336
+ "apple-ads": {
337
+ "command": "npx",
338
+ "args": ["-y", "@nerdsnipe-inc/apple-ads-mcp-server"],
339
+ "env": {
340
+ "APPLE_ADS_CLIENT_ID": "${APPLE_ADS_CLIENT_ID}",
341
+ "APPLE_ADS_TEAM_ID": "${APPLE_ADS_TEAM_ID}",
342
+ "APPLE_ADS_KEY_ID": "${APPLE_ADS_KEY_ID}",
343
+ "APPLE_ADS_PRIVATE_KEY_PATH": "${APPLE_ADS_PRIVATE_KEY_PATH}"
344
+ }
345
+ }
346
+ }
347
+ }
348
+ ```
349
+
350
+ Check it with `claude mcp list`.
351
+
352
+ ### Claude Desktop
353
+
354
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (Windows: `%APPDATA%\Claude\claude_desktop_config.json`), then fully quit and reopen the app:
355
+
356
+ ```json
357
+ {
358
+ "mcpServers": {
359
+ "apple-ads": {
360
+ "command": "/absolute/path/to/npx",
361
+ "args": ["-y", "@nerdsnipe-inc/apple-ads-mcp-server"],
362
+ "env": {
363
+ "APPLE_ADS_CLIENT_ID": "SEARCHADS.xxxxxxxx-...",
364
+ "APPLE_ADS_TEAM_ID": "SEARCHADS.xxxxxxxx-...",
365
+ "APPLE_ADS_KEY_ID": "xxxxxxxx-...",
366
+ "APPLE_ADS_PRIVATE_KEY_PATH": "/Users/you/.appleads/private_keys/private-key.pem"
367
+ }
368
+ }
369
+ }
370
+ }
371
+ ```
372
+
373
+ ### Cursor / Windsurf
374
+
375
+ Add the same `mcpServers` block as in Claude Desktop to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project) or `~/.codeium/windsurf/mcp_config.json`.
376
+
377
+ ### VS Code (GitHub Copilot)
378
+
379
+ Create `.vscode/mcp.json` in your workspace. VS Code uses a `servers` key and can prompt for secrets instead of storing them:
380
+
381
+ ```json
382
+ {
383
+ "inputs": [
384
+ { "type": "promptString", "id": "ads-client-id", "description": "Apple Ads clientId" },
385
+ { "type": "promptString", "id": "ads-team-id", "description": "Apple Ads teamId" },
386
+ { "type": "promptString", "id": "ads-key-id", "description": "Apple Ads keyId" },
387
+ { "type": "promptString", "id": "ads-key-path", "description": "Path to private-key.pem" }
388
+ ],
389
+ "servers": {
390
+ "apple-ads": {
391
+ "type": "stdio",
392
+ "command": "npx",
393
+ "args": ["-y", "@nerdsnipe-inc/apple-ads-mcp-server"],
394
+ "env": {
395
+ "APPLE_ADS_CLIENT_ID": "${input:ads-client-id}",
396
+ "APPLE_ADS_TEAM_ID": "${input:ads-team-id}",
397
+ "APPLE_ADS_KEY_ID": "${input:ads-key-id}",
398
+ "APPLE_ADS_PRIVATE_KEY_PATH": "${input:ads-key-path}"
399
+ }
400
+ }
401
+ }
402
+ }
403
+ ```
404
+
405
+ ### Codex CLI
406
+
407
+ Add to `~/.codex/config.toml`:
408
+
409
+ ```toml
410
+ [mcp_servers.apple-ads]
411
+ command = "npx"
412
+ args = ["-y", "@nerdsnipe-inc/apple-ads-mcp-server"]
413
+
414
+ [mcp_servers.apple-ads.env]
415
+ APPLE_ADS_CLIENT_ID = "SEARCHADS.xxxxxxxx-..."
416
+ APPLE_ADS_TEAM_ID = "SEARCHADS.xxxxxxxx-..."
417
+ APPLE_ADS_KEY_ID = "xxxxxxxx-..."
418
+ APPLE_ADS_PRIVATE_KEY_PATH = "/Users/you/.appleads/private_keys/private-key.pem"
419
+ ```
420
+
421
+ ### Gemini CLI
422
+
423
+ Add the standard `mcpServers` block from [Quick start](#quick-start-no-local-install) to `~/.gemini/settings.json`.
424
+
425
+ ### Interactive installer (macOS)
426
+
427
+ If you cloned this repo, `scripts/install-global.sh` prompts for your credentials, copies the key to `~/.appleads/private_keys` (mode 600), and registers the server in Claude Code (user scope) and Claude Desktop.
428
+
429
+ ### From source
430
+
431
+ ```bash
432
+ git clone https://github.com/NerdSnipe-Inc/apple-ads-mcp-server.git
433
+ cd apple-ads-mcp-server && npm install && npm run build
434
+ ```
435
+
436
+ Then use `"command": "node"` with `"args": ["/absolute/path/to/apple-ads-mcp-server/dist/index.js"]` in any config above, or:
437
+
438
+ ```bash
439
+ claude mcp add apple-ads --scope user -e APPLE_ADS_CLIENT_ID=... -e APPLE_ADS_TEAM_ID=... \
440
+ -e APPLE_ADS_KEY_ID=... -e APPLE_ADS_PRIVATE_KEY_PATH=... \
441
+ -- node /absolute/path/to/apple-ads-mcp-server/dist/index.js
442
+ ```
443
+
444
+ ## Environment variables
445
+
446
+ | Variable | Required | Description |
447
+ | --- | --- | --- |
448
+ | `APPLE_ADS_CLIENT_ID` (`ASA_CLIENT_ID`) | yes | Client ID from Account Settings > API |
449
+ | `APPLE_ADS_TEAM_ID` (`ASA_TEAM_ID`) | yes | Team ID |
450
+ | `APPLE_ADS_KEY_ID` (`ASA_KEY_ID`) | yes | Key ID |
451
+ | `APPLE_ADS_PRIVATE_KEY_PATH` (`ASA_PRIVATE_KEY_PATH`) | yes* | Path to the PEM private key (`~` supported) |
452
+ | `APPLE_ADS_PRIVATE_KEY` (`ASA_PRIVATE_KEY`) | yes* | Inline PEM instead of a path (`\n` escapes allowed) |
453
+ | `APPLE_ADS_AD_ACCOUNT_ID` (`ASA_AD_ACCOUNT_ID`) | no | Default ad account sent as `X-AP-Context: adAccountId=<id>` |
454
+ | `APPLE_ADS_ORG_ID` (`ASA_ORG_ID`) | no | Default organization ID for org-level calls |
455
+ | `APPLE_ADS_READ_ONLY` | no | `true` hides every tool that can modify data |
456
+ | `APPLE_ADS_ENABLED_GROUPS` | no | Comma list; only these groups are exposed |
457
+ | `APPLE_ADS_DISABLED_GROUPS` | no | Comma list of groups to hide |
458
+ | `APPLE_ADS_TIMEOUT_MS` | no | Request timeout (default 30000) |
459
+ | `APPLE_ADS_MAX_RETRIES` | no | Retries for 429/5xx (default 4) |
460
+ | `APPLE_ADS_RETRY_BASE_MS` / `APPLE_ADS_RETRY_MAX_MS` | no | Backoff base/cap (default 2000/16000) |
461
+ | `APPLE_ADS_LOG_LEVEL` | no | `error`, `warn` (default), `info`, `debug` (stderr only) |
462
+ | `APPLE_ADS_BASE_URL`, `APPLE_ADS_TOKEN_URL` | no | Test-only overrides (local mock servers) |
463
+
464
+ *One of the two key variables is required. `ASA_*` names are accepted as aliases; `APPLE_ADS_*` wins.
465
+
466
+ ## Usage notes
467
+
468
+ - **Context = ad account.** Most calls need an ad account. Pass `adAccountId` on any tool, or set `APPLE_ADS_AD_ACCOUNT_ID`. If neither is set and your credentials reach exactly one ad account, it is discovered automatically. Run `apple_ads_get_user_acls` (or `apple_ads_server_status`) to list accounts.
469
+ - **Pagination.** Query tools take `pagination: {offset, pageSize, fetchTotalCount}` (page size max 1000); fetch the next page by adding `pageSize` to `offset`. List tools take `limit` and `offset`. Report tools take a date range and a `selector`.
470
+ - **Rate limits.** Responses carry `RateLimit-*` headers; on 429 the server waits for `Retry-After` and retries with capped backoff.
471
+ - **Token budget.** Listing all tools costs roughly 55k tokens. Use `APPLE_ADS_ENABLED_GROUPS` (for example `accounts,campaigns,ad_groups,keywords,reports_apps`) to trim.
472
+ - **Escape hatch.** `apple_ads_request` calls any `/v1` path with a method, query and body.
473
+
474
+ ## Safety switches
475
+
476
+ ```bash
477
+ APPLE_ADS_READ_ONLY=true # reporting only
478
+ APPLE_ADS_ENABLED_GROUPS=campaigns,reports_apps
479
+ APPLE_ADS_DISABLED_GROUPS=budget_orders
480
+ ```
481
+
482
+ Group names: `accounts, apps, eligibility, brands, location_groups, campaigns, ad_groups, ads, creatives, assets, keywords, negative_keywords, bulk, geo, product_pages, budget_orders, reports_apps, reports_brands, insights, recommendations, suggestions, change_history` (the three meta tools live in `meta`, which is always available unless disabled).
483
+
484
+ ## Troubleshooting
485
+
486
+ | Symptom | Fix |
487
+ | --- | --- |
488
+ | "Missing credentials" error naming variables | Set all of client ID, team ID, key ID and a key path/inline key |
489
+ | 401 after refresh | Wrong clientId/teamId/keyId, or the public key was not uploaded for that key |
490
+ | 403 | The API user lacks a role on that ad account or org |
491
+ | "ad account required" | Pass `adAccountId` or set `APPLE_ADS_AD_ACCOUNT_ID` |
492
+ | `spawn npx ENOENT` or server not found in a desktop app | Use the absolute path from `which npx` (or `which node`) as `command` |
493
+ | Old version keeps running | `npx` caches packages; pin a version (`@nerdsnipe-inc/apple-ads-mcp-server@1.0.1`) or run `npx clear-npx-cache` |
494
+ | Key errors | Must be an EC `prime256v1` private key in PEM format |
495
+
496
+ Run `npx -y @nerdsnipe-inc/apple-ads-mcp-server --list-tools` to verify installation without credentials.
497
+
498
+ ## Development
499
+
500
+ ```bash
501
+ npm install
502
+ npm run generate # regenerate tools, docs and README tables from the SDK
503
+ npm run typecheck && npm run build && npm test
504
+ ```
505
+
506
+ `npm run generate:check` fails when generated files are stale (used in CI).
507
+
508
+ ## Releasing
509
+
510
+ Publishing is automated by [.github/workflows/publish.yml](.github/workflows/publish.yml). Pushing a version tag builds, tests and publishes to npmjs.com and to GitHub Packages. The npm publish uses **trusted publishing (OIDC)**: there is no `NPM_TOKEN` secret, and provenance is attached automatically. This matters because npm is [phasing out granular access tokens that bypass 2FA](https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/) (they lose direct publishing around January 2027).
511
+
512
+ **One-time setup (first release):**
513
+
514
+ 1. Publish `1.0.0` once by hand so the package exists on npm. You are prompted for 2FA, and `prepublishOnly` rebuilds and tests first:
515
+
516
+ ```bash
517
+ npm login
518
+ npm publish --access public
519
+ ```
520
+
521
+ 2. On npmjs.com open the package > **Settings** > **Trusted publisher** > GitHub Actions, and enter: organization `NerdSnipe-Inc`, repository `apple-ads-mcp-server`, workflow filename `publish.yml`.
522
+ 3. Optional hardening: on the same settings page set publishing access to "Require two-factor authentication and disallow tokens".
523
+
524
+ **Every release after that:**
525
+
526
+ ```bash
527
+ npm version patch # or minor / major: bumps package.json and creates the vX.Y.Z tag
528
+ git push --follow-tags
529
+ ```
530
+
531
+ `GITHUB_TOKEN` is provided automatically for the GitHub Packages step. The workflow refuses to publish if the tag and `package.json` version differ.
532
+
533
+ ## License
534
+
535
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/index.js").catch((e) => { console.error(e); process.exit(1); });