appstore-api-mcp 1.3.0 → 1.5.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 +23 -0
- package/README.md +11 -1
- package/docs/TOOLS.md +62 -0
- package/package.json +1 -1
- package/src/client.js +16 -1
- package/src/index.js +461 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.5.0] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`apps_review_status`** — fleet review-status board: every app's current
|
|
11
|
+
version + state (waiting/in-review/rejected/ready) in one call, with a summary.
|
|
12
|
+
- **`submit_for_review`** — full submit flow (create submission → add version →
|
|
13
|
+
submit) and **`release_version`** for manually releasing an approved build.
|
|
14
|
+
- **`signing_health`** — flags certificates & provisioning profiles that are
|
|
15
|
+
expired/expiring (within `withinDays`) or invalid, across the account.
|
|
16
|
+
- **`update_in_app_purchase`** — edit an IAP's reference name / review note.
|
|
17
|
+
|
|
18
|
+
## [1.4.0] - 2026-06-02
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Provisioning & code signing:** `list_bundle_ids`, `register_bundle_id`,
|
|
22
|
+
`list_devices`, `register_device`, `list_certificates`, `create_certificate`,
|
|
23
|
+
`revoke_certificate`, `list_profiles`, `create_profile`, `download_profile`,
|
|
24
|
+
`delete_profile`.
|
|
25
|
+
- **Game Center:** `list_game_center_leaderboards`, `list_game_center_achievements`.
|
|
26
|
+
- **Automatic rate-limit handling** — the client backs off and retries on `429`/`503`
|
|
27
|
+
(honoring `Retry-After`, else exponential backoff), so large multi-app sweeps
|
|
28
|
+
don't fail. The latest `x-rate-limit` header is exposed on the client.
|
|
29
|
+
|
|
7
30
|
## [1.3.0] - 2026-06-02
|
|
8
31
|
|
|
9
32
|
### Added
|
package/README.md
CHANGED
|
@@ -250,7 +250,17 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
250
250
|
| ⭐ `list_customer_reviews` / `reply_to_customer_review` | Read reviews (filter by rating/territory, shows if you've replied) and post public replies. |
|
|
251
251
|
| ✈️ `list_builds` / `list_beta_groups` / `list_beta_testers` / `add_beta_tester` | TestFlight — builds, beta groups, testers, and inviting testers. |
|
|
252
252
|
| 🛒 `list_in_app_purchases` / `get_app_price_schedule` / `list_available_territories` / `get_age_rating` | Catalog, pricing, territory availability, and age-rating. |
|
|
253
|
-
| `
|
|
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
|
+
| 🗂️ `apps_review_status` | **Fleet review board** — every app's current version + state (waiting/in-review/rejected/ready) in one call. |
|
|
256
|
+
| 🚀 `submit_for_review` / `release_version` | Submit a version to Apple review (full flow) and release an approved build. |
|
|
257
|
+
| 🔐 `signing_health` | Flag **certificates & profiles expiring soon** (or invalid) across the account — catches CI breakage early. |
|
|
258
|
+
| `update_in_app_purchase` | Update an IAP's reference name / review note. |
|
|
259
|
+
| `raw_request` | Any method/path against the API — app previews, matchmaking, Xcode Cloud, anything not above. |
|
|
260
|
+
|
|
261
|
+
> ⚙️ **Rate-limit aware:** the client automatically backs off and retries on
|
|
262
|
+
> App Store Connect's `429` (hourly quota), so large sweeps (e.g. auditing 60+
|
|
263
|
+
> apps) don't fall over.
|
|
254
264
|
|
|
255
265
|
> 🛡️ **Dry-run:** the `update_*` tools accept `dryRun: true` — they return a field-by-field
|
|
256
266
|
> diff (old → new) with length/limit checks and **write nothing**. Drop the flag to apply.
|
package/docs/TOOLS.md
CHANGED
|
@@ -310,6 +310,68 @@ Invites a tester by email (emails a real person — confirm first).
|
|
|
310
310
|
### get_age_rating
|
|
311
311
|
- `appId` **(required)** — the app's age-rating declaration (content descriptors).
|
|
312
312
|
|
|
313
|
+
## Submission, release & fleet status
|
|
314
|
+
|
|
315
|
+
### apps_review_status
|
|
316
|
+
Fleet review-status board across all (or selected) apps. Read-only.
|
|
317
|
+
- `appIds` — limit to these apps; `platform`; `limit`
|
|
318
|
+
- **Returns:** `{ summary: { apps, byState }, apps: [{ name, version, state, platform }] }`
|
|
319
|
+
|
|
320
|
+
### submit_for_review
|
|
321
|
+
Submits a version to Apple review (create submission → add version → submit).
|
|
322
|
+
**Sends the app to review — confirm first.**
|
|
323
|
+
- `appId` **(required)**, `versionId` **(required)**, `platform` (default IOS)
|
|
324
|
+
|
|
325
|
+
### release_version
|
|
326
|
+
Release an approved version waiting for manual release (PENDING_DEVELOPER_RELEASE).
|
|
327
|
+
**Makes it live — confirm first.**
|
|
328
|
+
- `versionId` **(required)**
|
|
329
|
+
|
|
330
|
+
### update_in_app_purchase
|
|
331
|
+
- `inAppPurchaseId` **(required)**, `name`, `reviewNote`
|
|
332
|
+
|
|
333
|
+
## Code-signing health
|
|
334
|
+
|
|
335
|
+
### signing_health
|
|
336
|
+
Lists certificates and provisioning profiles, flagging expired/expiring (within
|
|
337
|
+
`withinDays`, default 30) and INVALID profiles. Read-only.
|
|
338
|
+
- `withinDays` — threshold in days
|
|
339
|
+
- **Returns:** `{ summary, certIssues, profileIssues }` (each with `daysLeft`).
|
|
340
|
+
|
|
341
|
+
## Provisioning & code signing
|
|
342
|
+
|
|
343
|
+
### list_bundle_ids
|
|
344
|
+
- `filterIdentifier`, `limit` (default 200)
|
|
345
|
+
|
|
346
|
+
### register_bundle_id
|
|
347
|
+
- `identifier` **(required)**, `name` **(required)**, `platform` (IOS/MAC_OS/UNIVERSAL)
|
|
348
|
+
|
|
349
|
+
### list_devices / register_device
|
|
350
|
+
- register: `name` **(required)**, `udid` **(required)**, `platform` (IOS/MAC_OS)
|
|
351
|
+
|
|
352
|
+
### list_certificates / create_certificate / revoke_certificate
|
|
353
|
+
- `create_certificate` needs a CSR you generate yourself (`csrContent` PEM) + `certificateType` (e.g. IOS_DISTRIBUTION).
|
|
354
|
+
- `revoke_certificate` — `certificateId`.
|
|
355
|
+
|
|
356
|
+
### list_profiles / create_profile / download_profile / delete_profile
|
|
357
|
+
- `create_profile` — `name`, `profileType` (IOS_APP_DEVELOPMENT/IOS_APP_STORE/IOS_APP_ADHOC), `bundleId` (resource id), `certificateIds[]`, `deviceIds[]` (dev/ad-hoc).
|
|
358
|
+
- `download_profile` — returns base64 `.mobileprovision` in `profileContent`.
|
|
359
|
+
|
|
360
|
+
## Game Center
|
|
361
|
+
|
|
362
|
+
### list_game_center_leaderboards / list_game_center_achievements
|
|
363
|
+
- `appId` **(required)** — requires Game Center enabled on the app.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Rate limits
|
|
368
|
+
|
|
369
|
+
App Store Connect allows ~3,500 requests/hour and returns `429` when exceeded.
|
|
370
|
+
The client handles this automatically: on `429`/`503` it waits (honoring
|
|
371
|
+
`Retry-After` when present, else exponential backoff up to ~16 s) and retries up
|
|
372
|
+
to 4 times. The latest `x-rate-limit` header is kept on the client for visibility.
|
|
373
|
+
See <https://developer.apple.com/documentation/appstoreconnectapi/identifying-rate-limits>.
|
|
374
|
+
|
|
313
375
|
---
|
|
314
376
|
|
|
315
377
|
## Raw API access
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-api-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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
|
@@ -1337,6 +1337,467 @@ const tools = [
|
|
|
1337
1337
|
}),
|
|
1338
1338
|
},
|
|
1339
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
|
+
|
|
1591
|
+
// ---- Submission, release & fleet status ----
|
|
1592
|
+
{
|
|
1593
|
+
name: "apps_review_status",
|
|
1594
|
+
description:
|
|
1595
|
+
"Fleet review-status board: for all (or selected) apps, returns the most relevant App Store version and its state (WAITING_FOR_REVIEW, IN_REVIEW, PENDING_DEVELOPER_RELEASE, REJECTED, PROCESSING_FOR_APP_STORE, READY_FOR_SALE, …) plus an account summary. One call instead of opening every app. Read-only.",
|
|
1596
|
+
inputSchema: {
|
|
1597
|
+
type: "object",
|
|
1598
|
+
properties: {
|
|
1599
|
+
appIds: { type: "array", items: { type: "string" } },
|
|
1600
|
+
platform: { type: "string", description: "IOS, MAC_OS, TV_OS, VISION_OS" },
|
|
1601
|
+
limit: { type: "number", description: "Audit at most this many apps" },
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
run: async (a) => {
|
|
1605
|
+
let apps = await client.getAll("/apps", { limit: 200 });
|
|
1606
|
+
if (a.appIds?.length) apps = apps.filter((x) => a.appIds.includes(x.id));
|
|
1607
|
+
if (a.limit) apps = apps.slice(0, a.limit);
|
|
1608
|
+
const IN_FLIGHT = new Set([
|
|
1609
|
+
"PREPARE_FOR_SUBMISSION",
|
|
1610
|
+
"WAITING_FOR_REVIEW",
|
|
1611
|
+
"IN_REVIEW",
|
|
1612
|
+
"PENDING_DEVELOPER_RELEASE",
|
|
1613
|
+
"PENDING_APPLE_RELEASE",
|
|
1614
|
+
"PROCESSING_FOR_APP_STORE",
|
|
1615
|
+
"METADATA_REJECTED",
|
|
1616
|
+
"REJECTED",
|
|
1617
|
+
"DEVELOPER_REJECTED",
|
|
1618
|
+
"INVALID_BINARY",
|
|
1619
|
+
"WAITING_FOR_EXPORT_COMPLIANCE",
|
|
1620
|
+
]);
|
|
1621
|
+
const rows = await mapLimit(apps, 6, async (app) => {
|
|
1622
|
+
try {
|
|
1623
|
+
const q = { limit: 10 };
|
|
1624
|
+
if (a.platform) q["filter[platform]"] = a.platform;
|
|
1625
|
+
const vers = await client.getAll(
|
|
1626
|
+
`/apps/${app.id}/appStoreVersions`,
|
|
1627
|
+
q,
|
|
1628
|
+
);
|
|
1629
|
+
const chosen =
|
|
1630
|
+
vers.find((v) => IN_FLIGHT.has(v.attributes.appStoreState)) ||
|
|
1631
|
+
vers.find((v) => v.attributes.appStoreState === "READY_FOR_SALE") ||
|
|
1632
|
+
vers[0];
|
|
1633
|
+
return {
|
|
1634
|
+
appId: app.id,
|
|
1635
|
+
name: app.attributes.name,
|
|
1636
|
+
version: chosen?.attributes?.versionString,
|
|
1637
|
+
state: chosen?.attributes?.appStoreState || "NO_VERSION",
|
|
1638
|
+
platform: chosen?.attributes?.platform,
|
|
1639
|
+
};
|
|
1640
|
+
} catch (e) {
|
|
1641
|
+
return { appId: app.id, name: app.attributes.name, error: e.message.slice(0, 80) };
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
const byState = {};
|
|
1645
|
+
for (const r of rows) {
|
|
1646
|
+
const s = r.state || (r.error ? "ERROR" : "unknown");
|
|
1647
|
+
byState[s] = (byState[s] || 0) + 1;
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
summary: { apps: rows.length, byState },
|
|
1651
|
+
apps: rows.sort((x, y) =>
|
|
1652
|
+
String(x.state).localeCompare(String(y.state)),
|
|
1653
|
+
),
|
|
1654
|
+
};
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
name: "submit_for_review",
|
|
1659
|
+
description:
|
|
1660
|
+
"Submit an App Store version for Apple review. Runs the full flow: create a review submission, add the version, and submit it. NOTE: this sends the app to Apple review — confirm with the user first. platform default IOS.",
|
|
1661
|
+
inputSchema: {
|
|
1662
|
+
type: "object",
|
|
1663
|
+
properties: {
|
|
1664
|
+
appId: { type: "string" },
|
|
1665
|
+
versionId: { type: "string", description: "appStoreVersion id to submit" },
|
|
1666
|
+
platform: { type: "string", description: "IOS (default), MAC_OS, TV_OS, VISION_OS" },
|
|
1667
|
+
},
|
|
1668
|
+
required: ["appId", "versionId"],
|
|
1669
|
+
},
|
|
1670
|
+
run: async (a) => {
|
|
1671
|
+
const sub = await client.post("/reviewSubmissions", {
|
|
1672
|
+
data: {
|
|
1673
|
+
type: "reviewSubmissions",
|
|
1674
|
+
attributes: { platform: a.platform || "IOS" },
|
|
1675
|
+
relationships: { app: { data: { type: "apps", id: a.appId } } },
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
const subId = sub.data.id;
|
|
1679
|
+
await client.post("/reviewSubmissionItems", {
|
|
1680
|
+
data: {
|
|
1681
|
+
type: "reviewSubmissionItems",
|
|
1682
|
+
relationships: {
|
|
1683
|
+
reviewSubmission: {
|
|
1684
|
+
data: { type: "reviewSubmissions", id: subId },
|
|
1685
|
+
},
|
|
1686
|
+
appStoreVersion: {
|
|
1687
|
+
data: { type: "appStoreVersions", id: a.versionId },
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
},
|
|
1691
|
+
});
|
|
1692
|
+
const submitted = await client.patch(`/reviewSubmissions/${subId}`, {
|
|
1693
|
+
data: {
|
|
1694
|
+
type: "reviewSubmissions",
|
|
1695
|
+
id: subId,
|
|
1696
|
+
attributes: { submitted: true },
|
|
1697
|
+
},
|
|
1698
|
+
});
|
|
1699
|
+
return { reviewSubmissionId: subId, result: submitted };
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: "release_version",
|
|
1704
|
+
description:
|
|
1705
|
+
"Release an approved version that's waiting for manual release (state PENDING_DEVELOPER_RELEASE). NOTE: this makes the version live on the App Store — confirm with the user first.",
|
|
1706
|
+
inputSchema: {
|
|
1707
|
+
type: "object",
|
|
1708
|
+
properties: { versionId: { type: "string" } },
|
|
1709
|
+
required: ["versionId"],
|
|
1710
|
+
},
|
|
1711
|
+
run: async (a) =>
|
|
1712
|
+
client.post("/appStoreVersionReleaseRequests", {
|
|
1713
|
+
data: {
|
|
1714
|
+
type: "appStoreVersionReleaseRequests",
|
|
1715
|
+
relationships: {
|
|
1716
|
+
appStoreVersion: {
|
|
1717
|
+
data: { type: "appStoreVersions", id: a.versionId },
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
},
|
|
1721
|
+
}),
|
|
1722
|
+
},
|
|
1723
|
+
|
|
1724
|
+
// ---- Code-signing health ----
|
|
1725
|
+
{
|
|
1726
|
+
name: "signing_health",
|
|
1727
|
+
description:
|
|
1728
|
+
"Code-signing health check: lists certificates and provisioning profiles, flagging any expired or expiring within `withinDays` (default 30) and any INVALID profiles. Catches silent CI breakage before it happens. Read-only.",
|
|
1729
|
+
inputSchema: {
|
|
1730
|
+
type: "object",
|
|
1731
|
+
properties: {
|
|
1732
|
+
withinDays: { type: "number", description: "Flag items expiring within N days (default 30)" },
|
|
1733
|
+
},
|
|
1734
|
+
},
|
|
1735
|
+
run: async (a) => {
|
|
1736
|
+
const within = a.withinDays ?? 30;
|
|
1737
|
+
const now = Date.now();
|
|
1738
|
+
const daysLeft = (d) =>
|
|
1739
|
+
d ? Math.round((new Date(d).getTime() - now) / 86400000) : null;
|
|
1740
|
+
const certs = await client.getAll("/certificates", { limit: 200 });
|
|
1741
|
+
const profs = await client.getAll("/profiles", { limit: 200 });
|
|
1742
|
+
const certRows = certs.map((c) => ({
|
|
1743
|
+
id: c.id,
|
|
1744
|
+
name: c.attributes.name,
|
|
1745
|
+
type: c.attributes.certificateType,
|
|
1746
|
+
expirationDate: c.attributes.expirationDate,
|
|
1747
|
+
daysLeft: daysLeft(c.attributes.expirationDate),
|
|
1748
|
+
}));
|
|
1749
|
+
const profRows = profs.map((p) => ({
|
|
1750
|
+
id: p.id,
|
|
1751
|
+
name: p.attributes.name,
|
|
1752
|
+
type: p.attributes.profileType,
|
|
1753
|
+
state: p.attributes.profileState,
|
|
1754
|
+
expirationDate: p.attributes.expirationDate,
|
|
1755
|
+
daysLeft: daysLeft(p.attributes.expirationDate),
|
|
1756
|
+
}));
|
|
1757
|
+
const certIssues = certRows.filter(
|
|
1758
|
+
(c) => c.daysLeft !== null && c.daysLeft <= within,
|
|
1759
|
+
);
|
|
1760
|
+
const profileIssues = profRows.filter(
|
|
1761
|
+
(p) =>
|
|
1762
|
+
p.state === "INVALID" ||
|
|
1763
|
+
(p.daysLeft !== null && p.daysLeft <= within),
|
|
1764
|
+
);
|
|
1765
|
+
return {
|
|
1766
|
+
summary: {
|
|
1767
|
+
certificates: certRows.length,
|
|
1768
|
+
profiles: profRows.length,
|
|
1769
|
+
certsExpiringOrExpired: certIssues.length,
|
|
1770
|
+
profilesInvalidOrExpiring: profileIssues.length,
|
|
1771
|
+
withinDays: within,
|
|
1772
|
+
},
|
|
1773
|
+
certIssues: certIssues.sort((x, y) => (x.daysLeft ?? 0) - (y.daysLeft ?? 0)),
|
|
1774
|
+
profileIssues: profileIssues.sort((x, y) => (x.daysLeft ?? 0) - (y.daysLeft ?? 0)),
|
|
1775
|
+
};
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
name: "update_in_app_purchase",
|
|
1780
|
+
description:
|
|
1781
|
+
"Update an in-app purchase product's editable fields (reference name and/or review note). Pass the inAppPurchasesV2 id.",
|
|
1782
|
+
inputSchema: {
|
|
1783
|
+
type: "object",
|
|
1784
|
+
properties: {
|
|
1785
|
+
inAppPurchaseId: { type: "string" },
|
|
1786
|
+
name: { type: "string", description: "Reference name" },
|
|
1787
|
+
reviewNote: { type: "string" },
|
|
1788
|
+
},
|
|
1789
|
+
required: ["inAppPurchaseId"],
|
|
1790
|
+
},
|
|
1791
|
+
run: async (a) => {
|
|
1792
|
+
const attributes = {};
|
|
1793
|
+
if (a.name !== undefined) attributes.name = a.name;
|
|
1794
|
+
if (a.reviewNote !== undefined) attributes.reviewNote = a.reviewNote;
|
|
1795
|
+
return client.patch(`/inAppPurchasesV2/${a.inAppPurchaseId}`, {
|
|
1796
|
+
data: { type: "inAppPurchasesV2", id: a.inAppPurchaseId, attributes },
|
|
1797
|
+
});
|
|
1798
|
+
},
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1340
1801
|
// ---- Generic escape hatch ----
|
|
1341
1802
|
{
|
|
1342
1803
|
name: "raw_request",
|