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 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
- | `raw_request` | Any method/path against the API — previews, pricing, TestFlight, IAP, reviews, … |
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.2.0",
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",