appstore-api-mcp 1.2.0 → 1.4.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.
- package/CHANGELOG.md +21 -0
- package/README.md +38 -1
- package/docs/TOOLS.md +80 -0
- package/package.json +1 -1
- package/src/client.js +16 -1
- package/src/index.js +507 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.4.0] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Provisioning & code signing:** `list_bundle_ids`, `register_bundle_id`,
|
|
11
|
+
`list_devices`, `register_device`, `list_certificates`, `create_certificate`,
|
|
12
|
+
`revoke_certificate`, `list_profiles`, `create_profile`, `download_profile`,
|
|
13
|
+
`delete_profile`.
|
|
14
|
+
- **Game Center:** `list_game_center_leaderboards`, `list_game_center_achievements`.
|
|
15
|
+
- **Automatic rate-limit handling** — the client backs off and retries on `429`/`503`
|
|
16
|
+
(honoring `Retry-After`, else exponential backoff), so large multi-app sweeps
|
|
17
|
+
don't fail. The latest `x-rate-limit` header is exposed on the client.
|
|
18
|
+
|
|
19
|
+
## [1.3.0] - 2026-06-02
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Promoted popular App Store Connect capabilities from `raw_request` to dedicated tools:
|
|
23
|
+
- **Customer reviews:** `list_customer_reviews` (filter by rating/territory, shows reply status) + `reply_to_customer_review`.
|
|
24
|
+
- **TestFlight:** `list_builds`, `list_beta_groups`, `list_beta_testers`, `add_beta_tester`.
|
|
25
|
+
- **Catalog/pricing/availability:** `list_in_app_purchases`, `get_app_price_schedule`, `list_available_territories`, `get_age_rating`.
|
|
26
|
+
- All read tools validated end-to-end against a live account (reviews, builds, groups, testers, IAPs, 175 territories, age rating).
|
|
27
|
+
|
|
7
28
|
## [1.2.0] - 2026-06-02
|
|
8
29
|
|
|
9
30
|
### Added
|
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ whatever AI agent you already use.
|
|
|
50
50
|
- [Getting your API key](#getting-your-api-key) → full guide in [docs/SETUP.md](docs/SETUP.md)
|
|
51
51
|
- [Configuration](#configuration)
|
|
52
52
|
- [Tools](#tools) → full reference in [docs/TOOLS.md](docs/TOOLS.md)
|
|
53
|
+
- [What you can ask](#what-you-can-ask)
|
|
53
54
|
- [Analytics & reports setup](docs/ANALYTICS.md) — roles, Vendor Number, examples
|
|
54
55
|
- [Common workflows](#common-workflows)
|
|
55
56
|
- [Security](#security) → details in [docs/SECURITY.md](docs/SECURITY.md)
|
|
@@ -246,13 +247,49 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
246
247
|
| 🚀 `audit_apps` | **Fleet health check** — scan all (or selected) apps for missing subtitle/keywords/description, under-used keyword field, single-locale listings, missing screenshots, and more. Read-only. |
|
|
247
248
|
| 📊 `get_sales_report` / `get_subscription_report` / `get_finance_report` | Units/downloads, proceeds, **subscriptions & retention**, earnings by region (parsed rows). Needs a Vendor Number + a key with Admin/Finance/Sales role. |
|
|
248
249
|
| 📈 `request_analytics_report` → `list_analytics_reports` → `list_analytics_report_instances` → `get_analytics_report_data` | The async Analytics Reports API — downloads, sessions, active devices, App Store engagement. |
|
|
249
|
-
| `
|
|
250
|
+
| ⭐ `list_customer_reviews` / `reply_to_customer_review` | Read reviews (filter by rating/territory, shows if you've replied) and post public replies. |
|
|
251
|
+
| ✈️ `list_builds` / `list_beta_groups` / `list_beta_testers` / `add_beta_tester` | TestFlight — builds, beta groups, testers, and inviting testers. |
|
|
252
|
+
| 🛒 `list_in_app_purchases` / `get_app_price_schedule` / `list_available_territories` / `get_age_rating` | Catalog, pricing, territory availability, and age-rating. |
|
|
253
|
+
| 🔏 `list_bundle_ids` / `register_bundle_id` / `list_devices` / `register_device` / `list_certificates` / `create_certificate` / `revoke_certificate` / `list_profiles` / `create_profile` / `download_profile` / `delete_profile` | **Provisioning & code signing** — bundle IDs, devices, certificates, and provisioning profiles. |
|
|
254
|
+
| 🎮 `list_game_center_leaderboards` / `list_game_center_achievements` | Game Center leaderboards & achievements. |
|
|
255
|
+
| `raw_request` | Any method/path against the API — app previews, matchmaking, Xcode Cloud, anything not above. |
|
|
256
|
+
|
|
257
|
+
> ⚙️ **Rate-limit aware:** the client automatically backs off and retries on
|
|
258
|
+
> App Store Connect's `429` (hourly quota), so large sweeps (e.g. auditing 60+
|
|
259
|
+
> apps) don't fall over.
|
|
250
260
|
|
|
251
261
|
> 🛡️ **Dry-run:** the `update_*` tools accept `dryRun: true` — they return a field-by-field
|
|
252
262
|
> diff (old → new) with length/limit checks and **write nothing**. Drop the flag to apply.
|
|
253
263
|
|
|
254
264
|
---
|
|
255
265
|
|
|
266
|
+
## What you can ask
|
|
267
|
+
|
|
268
|
+
Once it's connected, just talk to your agent in plain language. A few things to try:
|
|
269
|
+
|
|
270
|
+
**Listings & ASO**
|
|
271
|
+
- *"Which of my apps are missing a subtitle or screenshots?"* (fleet audit)
|
|
272
|
+
- *"Tighten the keywords for <app> — dry-run it first so I can confirm."*
|
|
273
|
+
- *"Show me <app>'s current screenshots and flag any that look outdated."*
|
|
274
|
+
- *"Translate <app>'s description and keywords into German and French."*
|
|
275
|
+
|
|
276
|
+
**Performance**
|
|
277
|
+
- *"How many downloads did all my apps get last week?"*
|
|
278
|
+
- *"Pull last month's proceeds broken down by country."*
|
|
279
|
+
- *"How many active subscribers do I have, and what's the recent churn?"*
|
|
280
|
+
|
|
281
|
+
**Releases & the rest of the API** (via `raw_request`)
|
|
282
|
+
- *"Create a new App Store version (1.4.0) for <app>."*
|
|
283
|
+
- *"Summarize this week's 1-star reviews and draft polite replies."*
|
|
284
|
+
- *"List my latest TestFlight builds and which beta groups can see them."*
|
|
285
|
+
- *"What's <app>'s price and which territories is it available in?"*
|
|
286
|
+
|
|
287
|
+
> **Dedicated tools cover it all:** app management, localization, screenshots
|
|
288
|
+
> (incl. *seeing* them), sales/subscriptions/analytics, ASO audit, dry-run,
|
|
289
|
+
> **customer reviews, TestFlight, in-app purchases, pricing, availability, and
|
|
290
|
+
> age-rating**. Anything else in the App Store Connect API is still reachable
|
|
291
|
+
> through the **`raw_request`** escape hatch — just ask.
|
|
292
|
+
|
|
256
293
|
## Common workflows
|
|
257
294
|
|
|
258
295
|
### Update keywords or description
|
package/docs/TOOLS.md
CHANGED
|
@@ -266,6 +266,86 @@ Download + decompress + parse an instance's segments into rows.
|
|
|
266
266
|
|
|
267
267
|
---
|
|
268
268
|
|
|
269
|
+
## Customer reviews
|
|
270
|
+
|
|
271
|
+
### list_customer_reviews
|
|
272
|
+
- `appId` **(required)**
|
|
273
|
+
- `rating` — filter to a star rating 1–5
|
|
274
|
+
- `territory` — 3-letter code, e.g. `USA`, `GBR`
|
|
275
|
+
- `sort` — `-createdDate` (default), `createdDate`, `rating`, `-rating`
|
|
276
|
+
- `limit` — max reviews (default 50)
|
|
277
|
+
- **Returns:** reviews with `rating`, `title`, `body`, `reviewerNickname`, `createdDate`, `territory`, and `hasResponse`.
|
|
278
|
+
|
|
279
|
+
### reply_to_customer_review
|
|
280
|
+
Posts a **public** reply (confirm the text with the user first).
|
|
281
|
+
- `reviewId` **(required)**
|
|
282
|
+
- `responseBody` **(required)** — max ~5970 chars
|
|
283
|
+
|
|
284
|
+
## TestFlight
|
|
285
|
+
|
|
286
|
+
### list_builds
|
|
287
|
+
- `appId` **(required)**, `limit` (default 25) — newest first: `version`, `processingState`, upload/expiration dates, min OS.
|
|
288
|
+
|
|
289
|
+
### list_beta_groups
|
|
290
|
+
- `appId` **(required)** — group `name`, internal/external, public-link status.
|
|
291
|
+
|
|
292
|
+
### list_beta_testers
|
|
293
|
+
- `appId` **or** `betaGroupId` **(one required)**, `limit` (default 100).
|
|
294
|
+
|
|
295
|
+
### add_beta_tester
|
|
296
|
+
Invites a tester by email (emails a real person — confirm first).
|
|
297
|
+
- `betaGroupId` **(required)**, `email` **(required)**, `firstName`, `lastName`
|
|
298
|
+
|
|
299
|
+
## Catalog, pricing & availability
|
|
300
|
+
|
|
301
|
+
### list_in_app_purchases
|
|
302
|
+
- `appId` **(required)** — `name`, `productId`, type, `state`.
|
|
303
|
+
|
|
304
|
+
### get_app_price_schedule
|
|
305
|
+
- `appId` **(required)** — base territory + manual price points (raw schedule for inspection).
|
|
306
|
+
|
|
307
|
+
### list_available_territories
|
|
308
|
+
- `appId` **(required)**, `limit` (default 200) — `{ count, territories: [{ territory, available, releaseDate }] }`.
|
|
309
|
+
|
|
310
|
+
### get_age_rating
|
|
311
|
+
- `appId` **(required)** — the app's age-rating declaration (content descriptors).
|
|
312
|
+
|
|
313
|
+
## Provisioning & code signing
|
|
314
|
+
|
|
315
|
+
### list_bundle_ids
|
|
316
|
+
- `filterIdentifier`, `limit` (default 200)
|
|
317
|
+
|
|
318
|
+
### register_bundle_id
|
|
319
|
+
- `identifier` **(required)**, `name` **(required)**, `platform` (IOS/MAC_OS/UNIVERSAL)
|
|
320
|
+
|
|
321
|
+
### list_devices / register_device
|
|
322
|
+
- register: `name` **(required)**, `udid` **(required)**, `platform` (IOS/MAC_OS)
|
|
323
|
+
|
|
324
|
+
### list_certificates / create_certificate / revoke_certificate
|
|
325
|
+
- `create_certificate` needs a CSR you generate yourself (`csrContent` PEM) + `certificateType` (e.g. IOS_DISTRIBUTION).
|
|
326
|
+
- `revoke_certificate` — `certificateId`.
|
|
327
|
+
|
|
328
|
+
### list_profiles / create_profile / download_profile / delete_profile
|
|
329
|
+
- `create_profile` — `name`, `profileType` (IOS_APP_DEVELOPMENT/IOS_APP_STORE/IOS_APP_ADHOC), `bundleId` (resource id), `certificateIds[]`, `deviceIds[]` (dev/ad-hoc).
|
|
330
|
+
- `download_profile` — returns base64 `.mobileprovision` in `profileContent`.
|
|
331
|
+
|
|
332
|
+
## Game Center
|
|
333
|
+
|
|
334
|
+
### list_game_center_leaderboards / list_game_center_achievements
|
|
335
|
+
- `appId` **(required)** — requires Game Center enabled on the app.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Rate limits
|
|
340
|
+
|
|
341
|
+
App Store Connect allows ~3,500 requests/hour and returns `429` when exceeded.
|
|
342
|
+
The client handles this automatically: on `429`/`503` it waits (honoring
|
|
343
|
+
`Retry-After` when present, else exponential backoff up to ~16 s) and retries up
|
|
344
|
+
to 4 times. The latest `x-rate-limit` header is kept on the client for visibility.
|
|
345
|
+
See <https://developer.apple.com/documentation/appstoreconnectapi/identifying-rate-limits>.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
269
349
|
## Raw API access
|
|
270
350
|
|
|
271
351
|
### `raw_request`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-api-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MCP server for Apple App Store Connect — edit listings (keywords, descriptions, titles, screenshots), track analytics (downloads, proceeds, subscriptions, retention), run a fleet-wide ASO audit, preview changes with dry-run, and reach the full API. Works with any MCP client (Claude, Codex, Cursor, Windsurf, VS Code, Zed, Gemini CLI, Antigravity, Amazon Q, Goose, and more).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/client.js
CHANGED
|
@@ -75,7 +75,7 @@ export class AppStoreConnectClient {
|
|
|
75
75
|
return url;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
async request(method, path, { query, body } = {}) {
|
|
78
|
+
async request(method, path, { query, body, _attempt = 0 } = {}) {
|
|
79
79
|
const token = await this._getToken();
|
|
80
80
|
const url = this._buildUrl(path, query);
|
|
81
81
|
const headers = { Authorization: `Bearer ${token}` };
|
|
@@ -85,6 +85,21 @@ export class AppStoreConnectClient {
|
|
|
85
85
|
payload = JSON.stringify(body);
|
|
86
86
|
}
|
|
87
87
|
const res = await fetch(url, { method, headers, body: payload });
|
|
88
|
+
|
|
89
|
+
// Rate limiting: App Store Connect returns 429 when the hourly quota is hit.
|
|
90
|
+
// Respect Retry-After if present, else exponential backoff; retry a few times.
|
|
91
|
+
if ((res.status === 429 || res.status === 503) && _attempt < 4) {
|
|
92
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
93
|
+
const waitMs =
|
|
94
|
+
retryAfter > 0
|
|
95
|
+
? retryAfter * 1000
|
|
96
|
+
: Math.min(1000 * 2 ** _attempt, 16000);
|
|
97
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
98
|
+
return this.request(method, path, { query, body, _attempt: _attempt + 1 });
|
|
99
|
+
}
|
|
100
|
+
// Expose remaining quota for visibility (parsed from x-rate-limit header).
|
|
101
|
+
this.rateLimit = res.headers.get("x-rate-limit") || this.rateLimit;
|
|
102
|
+
|
|
88
103
|
const text = await res.text();
|
|
89
104
|
let data = null;
|
|
90
105
|
if (text) {
|
package/src/index.js
CHANGED
|
@@ -1081,6 +1081,513 @@ const tools = [
|
|
|
1081
1081
|
},
|
|
1082
1082
|
},
|
|
1083
1083
|
|
|
1084
|
+
// ---- Customer reviews ----
|
|
1085
|
+
{
|
|
1086
|
+
name: "list_customer_reviews",
|
|
1087
|
+
description:
|
|
1088
|
+
"List customer reviews for an app. Filter by rating (1–5) and/or territory (3-letter code, e.g. USA, GBR). Sorted newest-first by default. Each review includes whether you've already responded.",
|
|
1089
|
+
inputSchema: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: {
|
|
1092
|
+
appId: { type: "string" },
|
|
1093
|
+
rating: { type: "number", description: "Filter to a star rating 1–5" },
|
|
1094
|
+
territory: { type: "string", description: "3-letter territory code, e.g. USA" },
|
|
1095
|
+
sort: {
|
|
1096
|
+
type: "string",
|
|
1097
|
+
description: "'-createdDate' (default, newest first), 'createdDate', 'rating', '-rating'",
|
|
1098
|
+
},
|
|
1099
|
+
limit: { type: "number", description: "Max reviews (default 50)" },
|
|
1100
|
+
},
|
|
1101
|
+
required: ["appId"],
|
|
1102
|
+
},
|
|
1103
|
+
run: async (a) => {
|
|
1104
|
+
const query = {
|
|
1105
|
+
sort: a.sort || "-createdDate",
|
|
1106
|
+
limit: a.limit ?? 50,
|
|
1107
|
+
include: "response",
|
|
1108
|
+
};
|
|
1109
|
+
if (a.rating !== undefined) query["filter[rating]"] = a.rating;
|
|
1110
|
+
if (a.territory) query["filter[territory]"] = a.territory;
|
|
1111
|
+
const data = await client.getAll(
|
|
1112
|
+
`/apps/${a.appId}/customerReviews`,
|
|
1113
|
+
query,
|
|
1114
|
+
);
|
|
1115
|
+
return data.map((x) => ({
|
|
1116
|
+
id: x.id,
|
|
1117
|
+
...x.attributes,
|
|
1118
|
+
hasResponse: !!(x.relationships?.response?.data),
|
|
1119
|
+
}));
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
name: "reply_to_customer_review",
|
|
1124
|
+
description:
|
|
1125
|
+
"Publicly reply to a customer review. NOTE: this publishes a response visible on the App Store — confirm the text with the user first. responseBody max ~5970 chars.",
|
|
1126
|
+
inputSchema: {
|
|
1127
|
+
type: "object",
|
|
1128
|
+
properties: {
|
|
1129
|
+
reviewId: { type: "string" },
|
|
1130
|
+
responseBody: { type: "string" },
|
|
1131
|
+
},
|
|
1132
|
+
required: ["reviewId", "responseBody"],
|
|
1133
|
+
},
|
|
1134
|
+
run: async (a) =>
|
|
1135
|
+
client.post("/customerReviewResponses", {
|
|
1136
|
+
data: {
|
|
1137
|
+
type: "customerReviewResponses",
|
|
1138
|
+
attributes: { responseBody: a.responseBody },
|
|
1139
|
+
relationships: {
|
|
1140
|
+
review: { data: { type: "customerReviews", id: a.reviewId } },
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
}),
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
// ---- TestFlight ----
|
|
1147
|
+
{
|
|
1148
|
+
name: "list_builds",
|
|
1149
|
+
description:
|
|
1150
|
+
"List TestFlight builds for an app (newest first): version, upload/expiration dates, processing state, min OS.",
|
|
1151
|
+
inputSchema: {
|
|
1152
|
+
type: "object",
|
|
1153
|
+
properties: {
|
|
1154
|
+
appId: { type: "string" },
|
|
1155
|
+
limit: { type: "number", description: "Max builds (default 25)" },
|
|
1156
|
+
},
|
|
1157
|
+
required: ["appId"],
|
|
1158
|
+
},
|
|
1159
|
+
run: async (a) => {
|
|
1160
|
+
// The /builds collection supports sort; the app relationship does not.
|
|
1161
|
+
const data = await client.getAll(`/builds`, {
|
|
1162
|
+
"filter[app]": a.appId,
|
|
1163
|
+
sort: "-version",
|
|
1164
|
+
limit: a.limit ?? 25,
|
|
1165
|
+
});
|
|
1166
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
name: "list_beta_groups",
|
|
1171
|
+
description:
|
|
1172
|
+
"List TestFlight beta groups for an app (internal/external, public-link status).",
|
|
1173
|
+
inputSchema: {
|
|
1174
|
+
type: "object",
|
|
1175
|
+
properties: { appId: { type: "string" } },
|
|
1176
|
+
required: ["appId"],
|
|
1177
|
+
},
|
|
1178
|
+
run: async (a) => {
|
|
1179
|
+
const data = await client.getAll(`/apps/${a.appId}/betaGroups`);
|
|
1180
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "list_beta_testers",
|
|
1185
|
+
description:
|
|
1186
|
+
"List TestFlight beta testers — either for a whole app (pass appId) or a specific group (pass betaGroupId).",
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
appId: { type: "string" },
|
|
1191
|
+
betaGroupId: { type: "string" },
|
|
1192
|
+
limit: { type: "number", description: "Max testers (default 100)" },
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
run: async (a) => {
|
|
1196
|
+
let data;
|
|
1197
|
+
if (a.betaGroupId)
|
|
1198
|
+
data = await client.getAll(
|
|
1199
|
+
`/betaGroups/${a.betaGroupId}/betaTesters`,
|
|
1200
|
+
{ limit: a.limit ?? 100 },
|
|
1201
|
+
);
|
|
1202
|
+
else if (a.appId)
|
|
1203
|
+
data = await client.getAll(`/betaTesters`, {
|
|
1204
|
+
"filter[apps]": a.appId,
|
|
1205
|
+
limit: a.limit ?? 100,
|
|
1206
|
+
});
|
|
1207
|
+
else throw new Error("Provide appId or betaGroupId.");
|
|
1208
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
name: "add_beta_tester",
|
|
1213
|
+
description:
|
|
1214
|
+
"Add a beta tester to a TestFlight group by email (sends them an invite). NOTE: this emails a real person — confirm with the user first.",
|
|
1215
|
+
inputSchema: {
|
|
1216
|
+
type: "object",
|
|
1217
|
+
properties: {
|
|
1218
|
+
betaGroupId: { type: "string" },
|
|
1219
|
+
email: { type: "string" },
|
|
1220
|
+
firstName: { type: "string" },
|
|
1221
|
+
lastName: { type: "string" },
|
|
1222
|
+
},
|
|
1223
|
+
required: ["betaGroupId", "email"],
|
|
1224
|
+
},
|
|
1225
|
+
run: async (a) => {
|
|
1226
|
+
const attributes = { email: a.email };
|
|
1227
|
+
if (a.firstName) attributes.firstName = a.firstName;
|
|
1228
|
+
if (a.lastName) attributes.lastName = a.lastName;
|
|
1229
|
+
return client.post("/betaTesters", {
|
|
1230
|
+
data: {
|
|
1231
|
+
type: "betaTesters",
|
|
1232
|
+
attributes,
|
|
1233
|
+
relationships: {
|
|
1234
|
+
betaGroups: { data: [{ type: "betaGroups", id: a.betaGroupId }] },
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
|
|
1241
|
+
// ---- Catalog, pricing & availability ----
|
|
1242
|
+
{
|
|
1243
|
+
name: "list_in_app_purchases",
|
|
1244
|
+
description:
|
|
1245
|
+
"List the in-app purchase products for an app (name, product id, type, state).",
|
|
1246
|
+
inputSchema: {
|
|
1247
|
+
type: "object",
|
|
1248
|
+
properties: { appId: { type: "string" } },
|
|
1249
|
+
required: ["appId"],
|
|
1250
|
+
},
|
|
1251
|
+
run: async (a) => {
|
|
1252
|
+
const data = await client.getAll(`/apps/${a.appId}/inAppPurchasesV2`);
|
|
1253
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "list_available_territories",
|
|
1258
|
+
description:
|
|
1259
|
+
"List the territories (countries/regions) where an app is available. Returns territory codes and currencies.",
|
|
1260
|
+
inputSchema: {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
properties: {
|
|
1263
|
+
appId: { type: "string" },
|
|
1264
|
+
limit: { type: "number", description: "Max territories (default 200)" },
|
|
1265
|
+
},
|
|
1266
|
+
required: ["appId"],
|
|
1267
|
+
},
|
|
1268
|
+
run: async (a) => {
|
|
1269
|
+
// v2 availability model: app → appAvailabilityV2 → territoryAvailabilities.
|
|
1270
|
+
// Follow the relationship's own related link (it points at the /v2 API).
|
|
1271
|
+
const av = await client.get(`/apps/${a.appId}/appAvailabilityV2`);
|
|
1272
|
+
const related =
|
|
1273
|
+
av.data?.relationships?.territoryAvailabilities?.links?.related;
|
|
1274
|
+
if (!related) return { note: "No availability record for this app." };
|
|
1275
|
+
// Apple caps page size at 200; getAll paginates to cover the rest.
|
|
1276
|
+
const data = await client.getAll(related, {
|
|
1277
|
+
limit: Math.min(a.limit ?? 200, 200),
|
|
1278
|
+
});
|
|
1279
|
+
const territories = data
|
|
1280
|
+
.map((x) => {
|
|
1281
|
+
// The territory code is base64-encoded JSON in the item id: {"s":appId,"t":"USA"}.
|
|
1282
|
+
let territory = null;
|
|
1283
|
+
try {
|
|
1284
|
+
territory = JSON.parse(
|
|
1285
|
+
Buffer.from(x.id, "base64").toString("utf8"),
|
|
1286
|
+
).t;
|
|
1287
|
+
} catch {
|
|
1288
|
+
/* ignore */
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
territory,
|
|
1292
|
+
available: x.attributes?.available,
|
|
1293
|
+
releaseDate: x.attributes?.releaseDate,
|
|
1294
|
+
};
|
|
1295
|
+
})
|
|
1296
|
+
.filter((x) => x.territory);
|
|
1297
|
+
return {
|
|
1298
|
+
availableInNewTerritories: av.data?.attributes?.availableInNewTerritories,
|
|
1299
|
+
count: territories.length,
|
|
1300
|
+
territories,
|
|
1301
|
+
};
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
name: "get_age_rating",
|
|
1306
|
+
description:
|
|
1307
|
+
"Get an app's age-rating declaration (the content descriptors that determine its age rating).",
|
|
1308
|
+
inputSchema: {
|
|
1309
|
+
type: "object",
|
|
1310
|
+
properties: { appId: { type: "string" } },
|
|
1311
|
+
required: ["appId"],
|
|
1312
|
+
},
|
|
1313
|
+
run: async (a) => {
|
|
1314
|
+
const res = await client.get(`/apps/${a.appId}/appInfos`, {
|
|
1315
|
+
include: "ageRatingDeclaration",
|
|
1316
|
+
limit: 1,
|
|
1317
|
+
});
|
|
1318
|
+
const decl = (res.included || []).find(
|
|
1319
|
+
(x) => x.type === "ageRatingDeclarations",
|
|
1320
|
+
);
|
|
1321
|
+
return decl ? { id: decl.id, ...decl.attributes } : { note: "No age-rating declaration found." };
|
|
1322
|
+
},
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: "get_app_price_schedule",
|
|
1326
|
+
description:
|
|
1327
|
+
"Get an app's price schedule (base territory + manual price points). Pricing in the App Store Connect API is multi-step; this returns the schedule with its included prices for inspection.",
|
|
1328
|
+
inputSchema: {
|
|
1329
|
+
type: "object",
|
|
1330
|
+
properties: { appId: { type: "string" } },
|
|
1331
|
+
required: ["appId"],
|
|
1332
|
+
},
|
|
1333
|
+
run: async (a) =>
|
|
1334
|
+
client.get(`/apps/${a.appId}/appPriceSchedule`, {
|
|
1335
|
+
include: "baseTerritory,manualPrices",
|
|
1336
|
+
"limit[manualPrices]": 50,
|
|
1337
|
+
}),
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1340
|
+
// ---- Provisioning & code signing ----
|
|
1341
|
+
{
|
|
1342
|
+
name: "list_bundle_ids",
|
|
1343
|
+
description:
|
|
1344
|
+
"List registered Bundle IDs (app identifiers). Optionally filter by identifier.",
|
|
1345
|
+
inputSchema: {
|
|
1346
|
+
type: "object",
|
|
1347
|
+
properties: {
|
|
1348
|
+
filterIdentifier: { type: "string" },
|
|
1349
|
+
limit: { type: "number", description: "Max (default 200)" },
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
run: async (a) => {
|
|
1353
|
+
const q = { limit: a.limit ?? 200 };
|
|
1354
|
+
if (a.filterIdentifier) q["filter[identifier]"] = a.filterIdentifier;
|
|
1355
|
+
const data = await client.getAll("/bundleIds", q);
|
|
1356
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
name: "register_bundle_id",
|
|
1361
|
+
description:
|
|
1362
|
+
"Register a new Bundle ID. platform: IOS, MAC_OS, or UNIVERSAL.",
|
|
1363
|
+
inputSchema: {
|
|
1364
|
+
type: "object",
|
|
1365
|
+
properties: {
|
|
1366
|
+
identifier: { type: "string", description: "e.g. com.example.app" },
|
|
1367
|
+
name: { type: "string" },
|
|
1368
|
+
platform: { type: "string", description: "IOS (default), MAC_OS, UNIVERSAL" },
|
|
1369
|
+
},
|
|
1370
|
+
required: ["identifier", "name"],
|
|
1371
|
+
},
|
|
1372
|
+
run: async (a) =>
|
|
1373
|
+
client.post("/bundleIds", {
|
|
1374
|
+
data: {
|
|
1375
|
+
type: "bundleIds",
|
|
1376
|
+
attributes: {
|
|
1377
|
+
identifier: a.identifier,
|
|
1378
|
+
name: a.name,
|
|
1379
|
+
platform: a.platform || "IOS",
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
}),
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
name: "list_devices",
|
|
1386
|
+
description: "List registered devices (UDIDs) for ad-hoc/development distribution.",
|
|
1387
|
+
inputSchema: {
|
|
1388
|
+
type: "object",
|
|
1389
|
+
properties: { limit: { type: "number", description: "Max (default 200)" } },
|
|
1390
|
+
},
|
|
1391
|
+
run: async (a) => {
|
|
1392
|
+
const data = await client.getAll("/devices", { limit: a.limit ?? 200 });
|
|
1393
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "register_device",
|
|
1398
|
+
description: "Register a device by UDID. platform: IOS (default) or MAC_OS.",
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
type: "object",
|
|
1401
|
+
properties: {
|
|
1402
|
+
name: { type: "string" },
|
|
1403
|
+
udid: { type: "string" },
|
|
1404
|
+
platform: { type: "string", description: "IOS (default), MAC_OS" },
|
|
1405
|
+
},
|
|
1406
|
+
required: ["name", "udid"],
|
|
1407
|
+
},
|
|
1408
|
+
run: async (a) =>
|
|
1409
|
+
client.post("/devices", {
|
|
1410
|
+
data: {
|
|
1411
|
+
type: "devices",
|
|
1412
|
+
attributes: {
|
|
1413
|
+
name: a.name,
|
|
1414
|
+
udid: a.udid,
|
|
1415
|
+
platform: a.platform || "IOS",
|
|
1416
|
+
},
|
|
1417
|
+
},
|
|
1418
|
+
}),
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
name: "list_certificates",
|
|
1422
|
+
description: "List code-signing certificates (development & distribution).",
|
|
1423
|
+
inputSchema: {
|
|
1424
|
+
type: "object",
|
|
1425
|
+
properties: { limit: { type: "number", description: "Max (default 200)" } },
|
|
1426
|
+
},
|
|
1427
|
+
run: async (a) => {
|
|
1428
|
+
const data = await client.getAll("/certificates", { limit: a.limit ?? 200 });
|
|
1429
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
name: "create_certificate",
|
|
1434
|
+
description:
|
|
1435
|
+
"Create a code-signing certificate from a CSR. You must generate the Certificate Signing Request yourself (openssl) and pass its PEM as csrContent. certificateType e.g. IOS_DEVELOPMENT, IOS_DISTRIBUTION, DEVELOPMENT, DISTRIBUTION.",
|
|
1436
|
+
inputSchema: {
|
|
1437
|
+
type: "object",
|
|
1438
|
+
properties: {
|
|
1439
|
+
csrContent: { type: "string", description: "PEM contents of the CSR" },
|
|
1440
|
+
certificateType: { type: "string" },
|
|
1441
|
+
},
|
|
1442
|
+
required: ["csrContent", "certificateType"],
|
|
1443
|
+
},
|
|
1444
|
+
run: async (a) =>
|
|
1445
|
+
client.post("/certificates", {
|
|
1446
|
+
data: {
|
|
1447
|
+
type: "certificates",
|
|
1448
|
+
attributes: {
|
|
1449
|
+
csrContent: a.csrContent,
|
|
1450
|
+
certificateType: a.certificateType,
|
|
1451
|
+
},
|
|
1452
|
+
},
|
|
1453
|
+
}),
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
name: "revoke_certificate",
|
|
1457
|
+
description: "Revoke (delete) a code-signing certificate by id.",
|
|
1458
|
+
inputSchema: {
|
|
1459
|
+
type: "object",
|
|
1460
|
+
properties: { certificateId: { type: "string" } },
|
|
1461
|
+
required: ["certificateId"],
|
|
1462
|
+
},
|
|
1463
|
+
run: async (a) => {
|
|
1464
|
+
await client.delete(`/certificates/${a.certificateId}`);
|
|
1465
|
+
return { revoked: a.certificateId };
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
name: "list_profiles",
|
|
1470
|
+
description: "List provisioning profiles (includes the bundle id they target).",
|
|
1471
|
+
inputSchema: {
|
|
1472
|
+
type: "object",
|
|
1473
|
+
properties: { limit: { type: "number", description: "Max (default 200)" } },
|
|
1474
|
+
},
|
|
1475
|
+
run: async (a) => {
|
|
1476
|
+
const data = await client.getAll("/profiles", {
|
|
1477
|
+
limit: a.limit ?? 200,
|
|
1478
|
+
include: "bundleId",
|
|
1479
|
+
});
|
|
1480
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
name: "create_profile",
|
|
1485
|
+
description:
|
|
1486
|
+
"Create a provisioning profile. profileType e.g. IOS_APP_DEVELOPMENT, IOS_APP_STORE, IOS_APP_ADHOC. Pass the bundleId resource id, plus certificate ids (and device ids for development/ad-hoc).",
|
|
1487
|
+
inputSchema: {
|
|
1488
|
+
type: "object",
|
|
1489
|
+
properties: {
|
|
1490
|
+
name: { type: "string" },
|
|
1491
|
+
profileType: { type: "string" },
|
|
1492
|
+
bundleId: { type: "string", description: "bundleId resource id" },
|
|
1493
|
+
certificateIds: { type: "array", items: { type: "string" } },
|
|
1494
|
+
deviceIds: { type: "array", items: { type: "string" } },
|
|
1495
|
+
},
|
|
1496
|
+
required: ["name", "profileType", "bundleId", "certificateIds"],
|
|
1497
|
+
},
|
|
1498
|
+
run: async (a) => {
|
|
1499
|
+
const relationships = {
|
|
1500
|
+
bundleId: { data: { type: "bundleIds", id: a.bundleId } },
|
|
1501
|
+
certificates: {
|
|
1502
|
+
data: (a.certificateIds || []).map((id) => ({
|
|
1503
|
+
type: "certificates",
|
|
1504
|
+
id,
|
|
1505
|
+
})),
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
if (a.deviceIds && a.deviceIds.length)
|
|
1509
|
+
relationships.devices = {
|
|
1510
|
+
data: a.deviceIds.map((id) => ({ type: "devices", id })),
|
|
1511
|
+
};
|
|
1512
|
+
return client.post("/profiles", {
|
|
1513
|
+
data: {
|
|
1514
|
+
type: "profiles",
|
|
1515
|
+
attributes: { name: a.name, profileType: a.profileType },
|
|
1516
|
+
relationships,
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
},
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
name: "download_profile",
|
|
1523
|
+
description:
|
|
1524
|
+
"Get a provisioning profile's contents (base64 .mobileprovision in profileContent) by id.",
|
|
1525
|
+
inputSchema: {
|
|
1526
|
+
type: "object",
|
|
1527
|
+
properties: { profileId: { type: "string" } },
|
|
1528
|
+
required: ["profileId"],
|
|
1529
|
+
},
|
|
1530
|
+
run: async (a) => {
|
|
1531
|
+
const res = await client.get(`/profiles/${a.profileId}`);
|
|
1532
|
+
return { id: res.data.id, ...res.data.attributes };
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: "delete_profile",
|
|
1537
|
+
description: "Delete a provisioning profile by id.",
|
|
1538
|
+
inputSchema: {
|
|
1539
|
+
type: "object",
|
|
1540
|
+
properties: { profileId: { type: "string" } },
|
|
1541
|
+
required: ["profileId"],
|
|
1542
|
+
},
|
|
1543
|
+
run: async (a) => {
|
|
1544
|
+
await client.delete(`/profiles/${a.profileId}`);
|
|
1545
|
+
return { deleted: a.profileId };
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
|
|
1549
|
+
// ---- Game Center ----
|
|
1550
|
+
{
|
|
1551
|
+
name: "list_game_center_leaderboards",
|
|
1552
|
+
description:
|
|
1553
|
+
"List Game Center leaderboards for an app (requires Game Center enabled on the app).",
|
|
1554
|
+
inputSchema: {
|
|
1555
|
+
type: "object",
|
|
1556
|
+
properties: { appId: { type: "string" } },
|
|
1557
|
+
required: ["appId"],
|
|
1558
|
+
},
|
|
1559
|
+
run: async (a) => {
|
|
1560
|
+
const detail = await client.get(`/apps/${a.appId}/gameCenterDetail`);
|
|
1561
|
+
const detailId = detail.data && detail.data.id;
|
|
1562
|
+
if (!detailId)
|
|
1563
|
+
return { note: "Game Center is not enabled for this app." };
|
|
1564
|
+
const data = await client.getAll(
|
|
1565
|
+
`/gameCenterDetails/${detailId}/gameCenterLeaderboards`,
|
|
1566
|
+
);
|
|
1567
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: "list_game_center_achievements",
|
|
1572
|
+
description:
|
|
1573
|
+
"List Game Center achievements for an app (requires Game Center enabled on the app).",
|
|
1574
|
+
inputSchema: {
|
|
1575
|
+
type: "object",
|
|
1576
|
+
properties: { appId: { type: "string" } },
|
|
1577
|
+
required: ["appId"],
|
|
1578
|
+
},
|
|
1579
|
+
run: async (a) => {
|
|
1580
|
+
const detail = await client.get(`/apps/${a.appId}/gameCenterDetail`);
|
|
1581
|
+
const detailId = detail.data && detail.data.id;
|
|
1582
|
+
if (!detailId)
|
|
1583
|
+
return { note: "Game Center is not enabled for this app." };
|
|
1584
|
+
const data = await client.getAll(
|
|
1585
|
+
`/gameCenterDetails/${detailId}/gameCenterAchievements`,
|
|
1586
|
+
);
|
|
1587
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
|
|
1084
1591
|
// ---- Generic escape hatch ----
|
|
1085
1592
|
{
|
|
1086
1593
|
name: "raw_request",
|