appstore-api-mcp 1.4.0 → 1.6.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,31 @@ 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.6.0] - 2026-06-02
8
+
9
+ ### Added
10
+ - **`bulk_update_version_localizations`** — update listing copy across many
11
+ locales at once (creates missing ones); `dryRun` to preview.
12
+ - **`set_phased_release`** — control a version's 7-day gradual rollout (ACTIVE/PAUSE/COMPLETE).
13
+ - **Pricing:** `list_app_price_points` (find a territory's price points) and
14
+ `set_app_price` (set base price from a price point).
15
+ - **`list_app_store_version_experiments`** — Product Page Optimization A/B tests.
16
+
17
+ ### Changed
18
+ - README tools table: removed emojis from the tool-name column and moved them
19
+ into the description column.
20
+
21
+ ## [1.5.0] - 2026-06-02
22
+
23
+ ### Added
24
+ - **`apps_review_status`** — fleet review-status board: every app's current
25
+ version + state (waiting/in-review/rejected/ready) in one call, with a summary.
26
+ - **`submit_for_review`** — full submit flow (create submission → add version →
27
+ submit) and **`release_version`** for manually releasing an approved build.
28
+ - **`signing_health`** — flags certificates & provisioning profiles that are
29
+ expired/expiring (within `withinDays`) or invalid, across the account.
30
+ - **`update_in_app_purchase`** — edit an IAP's reference name / review note.
31
+
7
32
  ## [1.4.0] - 2026-06-02
8
33
 
9
34
  ### Added
package/README.md CHANGED
@@ -231,28 +231,29 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
231
231
  | Tool | What it does |
232
232
  | --- | --- |
233
233
  | `list_apps` / `get_app` | Browse your apps |
234
- | `list_app_infos` | Find the record holding name/subtitle/privacy localizations |
235
- | `list_app_info_localizations` | Read name, subtitle, privacy policy per locale |
236
- | `update_app_info_localization` | Update **name, subtitle, privacy policy** |
237
- | `create_app_info_localization` | Add a new locale's name/subtitle |
238
- | `list_app_store_versions` | List versions and their states |
239
- | `create_app_store_version` | Start a new version to prepare for submission |
240
- | `list_app_store_version_localizations` | Read description/keywords/etc. per locale |
241
- | `get_app_store_version_localization` | Read one locale's listing copy |
242
- | `update_app_store_version_localization` | Update **keywords, description, promo text, what's-new, URLs** |
243
- | `create_app_store_version_localization` | Add a new locale to a version |
234
+ | `list_app_infos` / `list_app_info_localizations` | Find/read the record holding name, subtitle & privacy policy per locale |
235
+ | `update_app_info_localization` / `create_app_info_localization` | Update or add a locale's **name, subtitle, privacy policy** |
236
+ | `list_app_store_versions` / `create_app_store_version` | List versions and their states; start a new version |
237
+ | `list_app_store_version_localizations` / `get_app_store_version_localization` | Read description / keywords / etc. per locale |
238
+ | `update_app_store_version_localization` / `create_app_store_version_localization` | Update or add a locale's **keywords, description, promo text, what's-new, URLs** |
239
+ | `bulk_update_version_localizations` | 🌍 Update listing copy across **many locales at once** (creates missing ones); `dryRun` to preview |
244
240
  | `list_screenshot_sets` / `create_screenshot_set` | Manage per-device screenshot sets |
245
241
  | `list_screenshots` / `upload_screenshot` / `delete_screenshot` | Manage screenshots (upload handles the full reserve→upload→commit flow) |
246
- | 👁️ `get_screenshot` | Fetch a live screenshot **as an image the agent can see** — review/compare what's currently on a listing |
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. |
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. |
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. |
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. |
242
+ | `get_screenshot` | 👁️ Fetch a live screenshot **as an image the agent can see** — review/compare what's on a listing |
243
+ | `audit_apps` | 🩺 **Fleet ASO audit** — scan all apps for missing subtitle/keywords/description, under-used keyword field, single-locale listings, missing screenshots. Read-only |
244
+ | `apps_review_status` | 🗂️ **Fleet review board** — every app's current version + state (waiting / in-review / rejected / ready) in one call |
245
+ | `submit_for_review` / `release_version` / `set_phased_release` | 🚀 Submit a version to Apple review (full flow), release an approved build, and control phased rollout |
246
+ | `get_sales_report` / `get_subscription_report` / `get_finance_report` | 📊 Units/downloads, proceeds, **subscriptions & retention**, earnings by region. Needs a Vendor Number + Admin/Finance/Sales key |
247
+ | `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 |
248
+ | `list_customer_reviews` / `reply_to_customer_review` | ⭐ Read reviews (filter by rating/territory, shows if you've replied) and post public replies |
249
+ | `list_builds` / `list_beta_groups` / `list_beta_testers` / `add_beta_tester` | ✈️ TestFlight — builds, beta groups, testers, and inviting testers |
250
+ | `list_in_app_purchases` / `update_in_app_purchase` | 🛒 In-app purchase products — list and edit name / review note |
251
+ | `list_app_price_points` / `set_app_price` / `get_app_price_schedule` / `list_available_territories` / `get_age_rating` | 💵 Pricing (find price points, set base price), territory availability, and age rating |
252
+ | `list_app_store_version_experiments` | 🧪 Product Page Optimization — list A/B tests (experiments) |
253
+ | `list_game_center_leaderboards` / `list_game_center_achievements` | 🎮 Game Center leaderboards & achievements |
254
+ | `signing_health` | 🔐 Flag **certificates & profiles expiring soon** (or invalid) across the account — catches CI breakage early |
255
+ | `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 |
256
+ | `raw_request` | 🧰 Any method/path against the API — app previews, matchmaking, Xcode Cloud, anything not above |
256
257
 
257
258
  > ⚙️ **Rate-limit aware:** the client automatically backs off and retries on
258
259
  > App Store Connect's `429` (hourly quota), so large sweeps (e.g. auditing 60+
package/docs/TOOLS.md CHANGED
@@ -310,6 +310,61 @@ 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
+ ### set_phased_release
331
+ Control a version's 7-day gradual rollout.
332
+ - `versionId` **(required)**, `state` **(required)** — `ACTIVE`, `PAUSE`, `COMPLETE`
333
+
334
+ ### update_in_app_purchase
335
+ - `inAppPurchaseId` **(required)**, `name`, `reviewNote`
336
+
337
+ ## Bulk localization
338
+
339
+ ### bulk_update_version_localizations
340
+ Update listing copy across many locales in one call (creates missing locales).
341
+ The agent does the translating; this writes them.
342
+ - `versionId` **(required)**
343
+ - `locales` **(required)** — array of `{ locale, description?, keywords?, promotionalText?, whatsNew?, marketingUrl?, supportUrl? }`
344
+ - `dryRun` — preview every create/update without writing
345
+
346
+ ## Pricing
347
+
348
+ ### list_app_price_points
349
+ - `appId` **(required)**, `territory` **(required)** — each point has `customerPrice`, proceeds, and the `id` for `set_app_price`.
350
+
351
+ ### set_app_price
352
+ Creates a new price schedule from a price point. **Changes live pricing — confirm first.**
353
+ - `appId` **(required)**, `baseTerritory` **(required)**, `pricePointId` **(required)**, `startDate` (YYYY-MM-DD, optional)
354
+
355
+ ## Product Page Optimization
356
+
357
+ ### list_app_store_version_experiments
358
+ - `appId` **(required)** — A/B tests with name, state, traffic proportion, start/end.
359
+
360
+ ## Code-signing health
361
+
362
+ ### signing_health
363
+ Lists certificates and provisioning profiles, flagging expired/expiring (within
364
+ `withinDays`, default 30) and INVALID profiles. Read-only.
365
+ - `withinDays` — threshold in days
366
+ - **Returns:** `{ summary, certIssues, profileIssues }` (each with `daysLeft`).
367
+
313
368
  ## Provisioning & code signing
314
369
 
315
370
  ### list_bundle_ids
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-api-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.6.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/index.js CHANGED
@@ -1588,6 +1588,419 @@ const tools = [
1588
1588
  },
1589
1589
  },
1590
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
+ {
1725
+ name: "set_phased_release",
1726
+ description:
1727
+ "Control a released version's phased (gradual 7-day) rollout. state: ACTIVE (start/resume), PAUSE, or COMPLETE (release to everyone now). Creates the phased release if one doesn't exist.",
1728
+ inputSchema: {
1729
+ type: "object",
1730
+ properties: {
1731
+ versionId: { type: "string" },
1732
+ state: { type: "string", description: "ACTIVE, PAUSE, or COMPLETE" },
1733
+ },
1734
+ required: ["versionId", "state"],
1735
+ },
1736
+ run: async (a) => {
1737
+ let pr = null;
1738
+ try {
1739
+ pr = await client.get(
1740
+ `/appStoreVersions/${a.versionId}/appStoreVersionPhasedRelease`,
1741
+ );
1742
+ } catch {
1743
+ /* none yet */
1744
+ }
1745
+ if (pr && pr.data && pr.data.id)
1746
+ return client.patch(`/appStoreVersionPhasedReleases/${pr.data.id}`, {
1747
+ data: {
1748
+ type: "appStoreVersionPhasedReleases",
1749
+ id: pr.data.id,
1750
+ attributes: { phasedReleaseState: a.state },
1751
+ },
1752
+ });
1753
+ return client.post(`/appStoreVersionPhasedReleases`, {
1754
+ data: {
1755
+ type: "appStoreVersionPhasedReleases",
1756
+ attributes: { phasedReleaseState: a.state },
1757
+ relationships: {
1758
+ appStoreVersion: {
1759
+ data: { type: "appStoreVersions", id: a.versionId },
1760
+ },
1761
+ },
1762
+ },
1763
+ });
1764
+ },
1765
+ },
1766
+
1767
+ // ---- Bulk localization ----
1768
+ {
1769
+ name: "bulk_update_version_localizations",
1770
+ description:
1771
+ "Update an App Store version's listing copy across MANY locales in one call — creating locales that don't exist yet. Pass an array of { locale, description?, keywords?, promotionalText?, whatsNew?, marketingUrl?, supportUrl? }. The agent does the translating; this writes them all. Set dryRun:true to preview every create/update without writing.",
1772
+ inputSchema: {
1773
+ type: "object",
1774
+ properties: {
1775
+ versionId: { type: "string" },
1776
+ locales: {
1777
+ type: "array",
1778
+ items: { type: "object", additionalProperties: true },
1779
+ description: "Array of { locale, ...fields } objects",
1780
+ },
1781
+ dryRun: { type: "boolean", description: "Preview without writing" },
1782
+ },
1783
+ required: ["versionId", "locales"],
1784
+ },
1785
+ run: async (a) => {
1786
+ const existing = await client.getAll(
1787
+ `/appStoreVersions/${a.versionId}/appStoreVersionLocalizations`,
1788
+ );
1789
+ const byLocale = Object.fromEntries(
1790
+ existing.map((x) => [x.attributes.locale, x]),
1791
+ );
1792
+ const FIELDS = [
1793
+ "description",
1794
+ "keywords",
1795
+ "promotionalText",
1796
+ "whatsNew",
1797
+ "marketingUrl",
1798
+ "supportUrl",
1799
+ ];
1800
+ const results = [];
1801
+ for (const item of a.locales) {
1802
+ const { locale } = item;
1803
+ const attributes = {};
1804
+ for (const k of FIELDS)
1805
+ if (item[k] !== undefined) attributes[k] = item[k];
1806
+ const warnings = validateAttributes(attributes);
1807
+ const ex = byLocale[locale];
1808
+ if (a.dryRun) {
1809
+ results.push({
1810
+ locale,
1811
+ action: ex ? "update" : "create",
1812
+ changes: ex
1813
+ ? buildDiff(ex.attributes, attributes)
1814
+ : Object.entries(attributes).map(([field, to]) => ({ field, to })),
1815
+ warnings,
1816
+ });
1817
+ continue;
1818
+ }
1819
+ if (ex) {
1820
+ await client.patch(
1821
+ `/appStoreVersionLocalizations/${ex.id}`,
1822
+ { data: { type: "appStoreVersionLocalizations", id: ex.id, attributes } },
1823
+ );
1824
+ results.push({ locale, action: "updated", id: ex.id, warnings });
1825
+ } else {
1826
+ const created = await client.post(`/appStoreVersionLocalizations`, {
1827
+ data: {
1828
+ type: "appStoreVersionLocalizations",
1829
+ attributes: { locale, ...attributes },
1830
+ relationships: {
1831
+ appStoreVersion: {
1832
+ data: { type: "appStoreVersions", id: a.versionId },
1833
+ },
1834
+ },
1835
+ },
1836
+ });
1837
+ results.push({ locale, action: "created", id: created.data.id, warnings });
1838
+ }
1839
+ }
1840
+ return { dryRun: !!a.dryRun, count: results.length, results };
1841
+ },
1842
+ },
1843
+
1844
+ // ---- Pricing ----
1845
+ {
1846
+ name: "list_app_price_points",
1847
+ description:
1848
+ "List available price points for an app in a territory — each has a customerPrice and your proceeds, plus the pricePointId to use with set_app_price.",
1849
+ inputSchema: {
1850
+ type: "object",
1851
+ properties: {
1852
+ appId: { type: "string" },
1853
+ territory: { type: "string", description: "3-letter code, e.g. USA" },
1854
+ limit: { type: "number", description: "Max (default 200)" },
1855
+ },
1856
+ required: ["appId", "territory"],
1857
+ },
1858
+ run: async (a) => {
1859
+ const data = await client.getAll(`/apps/${a.appId}/appPricePoints`, {
1860
+ "filter[territory]": a.territory,
1861
+ limit: Math.min(a.limit ?? 200, 200),
1862
+ });
1863
+ return data.map((x) => ({ id: x.id, ...x.attributes }));
1864
+ },
1865
+ },
1866
+ {
1867
+ name: "set_app_price",
1868
+ description:
1869
+ "Set an app's base price by creating a new price schedule from a price point (get one via list_app_price_points). NOTE: this changes LIVE pricing — confirm with the user. Optional startDate (YYYY-MM-DD) to schedule; omit for immediate.",
1870
+ inputSchema: {
1871
+ type: "object",
1872
+ properties: {
1873
+ appId: { type: "string" },
1874
+ baseTerritory: { type: "string", description: "3-letter code, e.g. USA" },
1875
+ pricePointId: { type: "string" },
1876
+ startDate: { type: "string", description: "YYYY-MM-DD (optional)" },
1877
+ },
1878
+ required: ["appId", "baseTerritory", "pricePointId"],
1879
+ },
1880
+ run: async (a) => {
1881
+ const lid = "new-manual-price";
1882
+ return client.post(`/appPriceSchedules`, {
1883
+ data: {
1884
+ type: "appPriceSchedules",
1885
+ relationships: {
1886
+ app: { data: { type: "apps", id: a.appId } },
1887
+ baseTerritory: {
1888
+ data: { type: "territories", id: a.baseTerritory },
1889
+ },
1890
+ manualPrices: { data: [{ type: "appPrices", id: lid }] },
1891
+ },
1892
+ },
1893
+ included: [
1894
+ {
1895
+ type: "appPrices",
1896
+ id: lid,
1897
+ attributes: a.startDate ? { startDate: a.startDate } : {},
1898
+ relationships: {
1899
+ appPricePoint: {
1900
+ data: { type: "appPricePoints", id: a.pricePointId },
1901
+ },
1902
+ },
1903
+ },
1904
+ ],
1905
+ });
1906
+ },
1907
+ },
1908
+
1909
+ // ---- Product Page Optimization (A/B testing) ----
1910
+ {
1911
+ name: "list_app_store_version_experiments",
1912
+ description:
1913
+ "List Product Page Optimization A/B tests (App Store version experiments) for an app — name, state, traffic proportion, and start/end.",
1914
+ inputSchema: {
1915
+ type: "object",
1916
+ properties: { appId: { type: "string" } },
1917
+ required: ["appId"],
1918
+ },
1919
+ run: async (a) => {
1920
+ const data = await client.getAll(
1921
+ `/apps/${a.appId}/appStoreVersionExperimentsV2`,
1922
+ );
1923
+ return data.map((x) => ({ id: x.id, ...x.attributes }));
1924
+ },
1925
+ },
1926
+
1927
+ // ---- Code-signing health ----
1928
+ {
1929
+ name: "signing_health",
1930
+ description:
1931
+ "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.",
1932
+ inputSchema: {
1933
+ type: "object",
1934
+ properties: {
1935
+ withinDays: { type: "number", description: "Flag items expiring within N days (default 30)" },
1936
+ },
1937
+ },
1938
+ run: async (a) => {
1939
+ const within = a.withinDays ?? 30;
1940
+ const now = Date.now();
1941
+ const daysLeft = (d) =>
1942
+ d ? Math.round((new Date(d).getTime() - now) / 86400000) : null;
1943
+ const certs = await client.getAll("/certificates", { limit: 200 });
1944
+ const profs = await client.getAll("/profiles", { limit: 200 });
1945
+ const certRows = certs.map((c) => ({
1946
+ id: c.id,
1947
+ name: c.attributes.name,
1948
+ type: c.attributes.certificateType,
1949
+ expirationDate: c.attributes.expirationDate,
1950
+ daysLeft: daysLeft(c.attributes.expirationDate),
1951
+ }));
1952
+ const profRows = profs.map((p) => ({
1953
+ id: p.id,
1954
+ name: p.attributes.name,
1955
+ type: p.attributes.profileType,
1956
+ state: p.attributes.profileState,
1957
+ expirationDate: p.attributes.expirationDate,
1958
+ daysLeft: daysLeft(p.attributes.expirationDate),
1959
+ }));
1960
+ const certIssues = certRows.filter(
1961
+ (c) => c.daysLeft !== null && c.daysLeft <= within,
1962
+ );
1963
+ const profileIssues = profRows.filter(
1964
+ (p) =>
1965
+ p.state === "INVALID" ||
1966
+ (p.daysLeft !== null && p.daysLeft <= within),
1967
+ );
1968
+ return {
1969
+ summary: {
1970
+ certificates: certRows.length,
1971
+ profiles: profRows.length,
1972
+ certsExpiringOrExpired: certIssues.length,
1973
+ profilesInvalidOrExpiring: profileIssues.length,
1974
+ withinDays: within,
1975
+ },
1976
+ certIssues: certIssues.sort((x, y) => (x.daysLeft ?? 0) - (y.daysLeft ?? 0)),
1977
+ profileIssues: profileIssues.sort((x, y) => (x.daysLeft ?? 0) - (y.daysLeft ?? 0)),
1978
+ };
1979
+ },
1980
+ },
1981
+ {
1982
+ name: "update_in_app_purchase",
1983
+ description:
1984
+ "Update an in-app purchase product's editable fields (reference name and/or review note). Pass the inAppPurchasesV2 id.",
1985
+ inputSchema: {
1986
+ type: "object",
1987
+ properties: {
1988
+ inAppPurchaseId: { type: "string" },
1989
+ name: { type: "string", description: "Reference name" },
1990
+ reviewNote: { type: "string" },
1991
+ },
1992
+ required: ["inAppPurchaseId"],
1993
+ },
1994
+ run: async (a) => {
1995
+ const attributes = {};
1996
+ if (a.name !== undefined) attributes.name = a.name;
1997
+ if (a.reviewNote !== undefined) attributes.reviewNote = a.reviewNote;
1998
+ return client.patch(`/inAppPurchasesV2/${a.inAppPurchaseId}`, {
1999
+ data: { type: "inAppPurchasesV2", id: a.inAppPurchaseId, attributes },
2000
+ });
2001
+ },
2002
+ },
2003
+
1591
2004
  // ---- Generic escape hatch ----
1592
2005
  {
1593
2006
  name: "raw_request",