appstore-api-mcp 1.16.4 → 1.17.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 +29 -0
- package/README.md +7 -1
- package/docs/RECIPES.md +19 -0
- package/docs/TOOLS.md +44 -2
- package/package.json +1 -1
- package/src/index.js +158 -4
- package/src/preflight-data/NOTICE.md +25 -0
- package/src/preflight-data/purpose_string_keys.json +703 -0
- package/src/preflight-data/required_reason_apis.json +343 -0
- package/src/preflight-data/sdks_requiring_manifest.json +591 -0
- package/src/preflight-rules.js +463 -0
- package/src/preflight-scan.js +615 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.17.0] - 2026-10-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Preflight guideline review before every submission.** A new read-only
|
|
11
|
+
`preflight_review` tool checks a version against common App Store Review
|
|
12
|
+
Guidelines and App Store Connect requirements, citing the Apple rule for each
|
|
13
|
+
finding and returning a `PASS / FAIL / WARN / MANUAL / ASC / NA` verdict with
|
|
14
|
+
evidence and a fix.
|
|
15
|
+
- *App Store Connect checks* (need `appId` + `versionId`): privacy-policy URL,
|
|
16
|
+
description and support URL per locale, screenshots per locale, media still
|
|
17
|
+
processing, age rating, build attached & VALID, per-build export compliance,
|
|
18
|
+
IDFA declaration, and a review demo account / notes when the app has a login.
|
|
19
|
+
- *Local source checks* (pass `projectRoot`, the app's source tree on this
|
|
20
|
+
machine): purpose strings for permission APIs, required-reason APIs declared
|
|
21
|
+
in a privacy manifest, SDKs that need a manifest, App Tracking Transparency,
|
|
22
|
+
account creation without in-app account deletion, Sign in with Apple,
|
|
23
|
+
deprecated `UIWebView`, private-API hints, placeholder content, insecure
|
|
24
|
+
HTTP, external payment SDKs, Restore Purchases, and the export-compliance key.
|
|
25
|
+
- `submit_for_review` and `release_pipeline` now run preflight automatically and
|
|
26
|
+
**refuse to submit when any finding is FAIL**. New `skipPreflight` (bypass the
|
|
27
|
+
guideline review; the opaque-409 diagnosis still runs) and `projectRoot`
|
|
28
|
+
(also scan local source) options on both.
|
|
29
|
+
|
|
30
|
+
The local scanner is a zero-dependency JavaScript port of the scanner from the
|
|
31
|
+
MIT-licensed [app-store-preflight](https://github.com/ilan-uzan/app-store-preflight)
|
|
32
|
+
project by Ilan Uzan; the bundled required-reason / purpose-string / SDK data
|
|
33
|
+
lists come from the same project (see `src/preflight-data/NOTICE.md`). The rules
|
|
34
|
+
that turn facts into guideline-cited verdicts are original to this project.
|
|
35
|
+
|
|
7
36
|
## [1.16.4] - 2026-10-08
|
|
8
37
|
|
|
9
38
|
### Fixed
|
package/README.md
CHANGED
|
@@ -264,7 +264,8 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
264
264
|
| `list_app_preview_sets` / `list_app_previews` / `get_app_preview` / `create_app_preview_set` / `upload_app_preview` / `delete_app_preview` | 🎬 **App preview videos** — list, inspect (incl. `videoUrl`), upload, delete |
|
|
265
265
|
| `audit_apps` | 🩺 **Fleet ASO audit** — scan all apps for missing subtitle/keywords/description, under-used keyword field, single-locale listings, missing screenshots. Read-only |
|
|
266
266
|
| `apps_review_status` | 🗂️ **Fleet review board** — every app's current version + state (waiting / in-review / rejected / ready) in one call |
|
|
267
|
-
| `submit_for_review` / `release_version` / `set_phased_release` | 🚀 Submit a version to Apple review (full flow), release an approved build, and control phased rollout |
|
|
267
|
+
| `submit_for_review` / `release_version` / `set_phased_release` | 🚀 Submit a version to Apple review (full flow, **gated by preflight**), release an approved build, and control phased rollout |
|
|
268
|
+
| `preflight_review` | 🛫 **Pre-submission guideline review** — checks the version (and, with `projectRoot`, the local source) against App Store Review Guidelines + App Store Connect requirements, citing the Apple rule for each finding. `submit_for_review` blocks on any `FAIL`. Read-only |
|
|
268
269
|
| `doctor` | 🩺 Diagnose setup: Node, creds, key works, role capabilities, vendor number, Mac build tools, write mode |
|
|
269
270
|
| `snapshot_app_metadata` / `diff_app_metadata_snapshot` / `restore_app_metadata` / `restore_screenshots` / `restore_app_previews` | 💾 Back up / compare / restore an app's metadata. Text always saved; `includeScreenshots:true` and `includePreviews:true` also download the images/videos so **deleted screenshots and previews can be re-uploaded**. Set `APPSTORE_MCP_AUTO_SNAPSHOT=true` to auto-snapshot text before the first edit |
|
|
270
271
|
| `release_readiness_check` | ✅ One-call **go/no-go report** — build, metadata, ASO, screenshots, compliance, TestFlight, reviews |
|
|
@@ -453,6 +454,11 @@ filling out a listing.
|
|
|
453
454
|
|
|
454
455
|
Built by **Sviatoslav (Sviat) Fil** — [filsv.com](https://filsv.com) — indie iOS apps & developer tools.
|
|
455
456
|
|
|
457
|
+
The `preflight_review` tool's local-project scanner and its required-reason /
|
|
458
|
+
purpose-string / SDK data lists are derived from the MIT-licensed
|
|
459
|
+
**[app-store-preflight](https://github.com/ilan-uzan/app-store-preflight)**
|
|
460
|
+
project by Ilan Uzan (see [`src/preflight-data/NOTICE.md`](src/preflight-data/NOTICE.md)).
|
|
461
|
+
|
|
456
462
|
## License
|
|
457
463
|
|
|
458
464
|
MIT © Sviatoslav Fil — see [LICENSE](LICENSE).
|
package/docs/RECIPES.md
CHANGED
|
@@ -35,6 +35,20 @@ blockers first.
|
|
|
35
35
|
|
|
36
36
|
Uses: `release_readiness_check` (single call), or the individual tools.
|
|
37
37
|
|
|
38
|
+
For a **guideline/metadata** review (not just completeness), add `preflight_review`
|
|
39
|
+
— it cites the App Store Review Guideline for each finding and marks blocking
|
|
40
|
+
`FAIL`s. Pass `projectRoot` to also scan the local source (purpose strings,
|
|
41
|
+
privacy manifests, account deletion, ATT, UIWebView, …):
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
Run a preflight review for AppName v1.4.0 against the App Store guidelines. My
|
|
45
|
+
Xcode project is at /path/to/AppName. Show the status, every FAIL with its
|
|
46
|
+
Apple rule and fix, then the warnings.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Uses: `preflight_review` (read-only). `submit_for_review` runs this automatically
|
|
50
|
+
and refuses to submit on any `FAIL`.
|
|
51
|
+
|
|
38
52
|
<details><summary>Example output (fake data)</summary>
|
|
39
53
|
|
|
40
54
|
| Area | Status | Detail |
|
|
@@ -65,6 +79,11 @@ Uses: `list_builds`, `list_beta_groups`, `list_beta_testers`,
|
|
|
65
79
|
`add_build_to_beta_group`, `submit_beta_review`, `submit_for_review`,
|
|
66
80
|
`release_version`, `set_phased_release`. **Stops at each human gate.**
|
|
67
81
|
|
|
82
|
+
> At gate (4), `submit_for_review` runs `diagnose_submission` **and**
|
|
83
|
+
> `preflight_review` first and refuses to submit on any `FAIL`. Pass
|
|
84
|
+
> `projectRoot` to also scan the local source, or `skipPreflight:true` to bypass
|
|
85
|
+
> the guideline review (the opaque-409 diagnosis still runs).
|
|
86
|
+
|
|
68
87
|
## 4. Customer reviews → release notes loop
|
|
69
88
|
|
|
70
89
|
```text
|
package/docs/TOOLS.md
CHANGED
|
@@ -319,8 +319,11 @@ Fleet review-status board across all (or selected) apps. Read-only.
|
|
|
319
319
|
|
|
320
320
|
### submit_for_review
|
|
321
321
|
Submits a version to Apple review (create submission → add version → submit).
|
|
322
|
-
**
|
|
322
|
+
Runs `diagnose_submission` **and** `preflight_review` first and refuses to submit
|
|
323
|
+
if either finds a blocker. **Sends the app to review — confirm first.**
|
|
323
324
|
- `appId` **(required)**, `versionId` **(required)**, `platform` (default IOS)
|
|
325
|
+
- `skipPreflight` — bypass the guideline review (the 409 diagnosis still runs)
|
|
326
|
+
- `projectRoot` — also scan the local source during preflight
|
|
324
327
|
|
|
325
328
|
### release_version
|
|
326
329
|
Release an approved version waiting for manual release (PENDING_DEVELOPER_RELEASE).
|
|
@@ -593,11 +596,50 @@ already in review.
|
|
|
593
596
|
|
|
594
597
|
### Diagnostics & orchestrators
|
|
595
598
|
- `diagnose_submission(appId, versionId)` — read-only: the exact blockers behind submit's 409.
|
|
599
|
+
- `preflight_review(appId?, versionId?, projectRoot?, only?)` — read-only guideline/metadata review (see below).
|
|
596
600
|
- `swap_build(appId, versionId, buildNumber|buildId)` — wait for processing → cancel in-flight review → attach.
|
|
597
|
-
- `release_pipeline(appId, versionId, buildNumber?, submit?)` — attach → diagnose → (if clean and `submit:true`) submit.
|
|
601
|
+
- `release_pipeline(appId, versionId, buildNumber?, submit?, skipPreflight?, projectRoot?)` — attach → diagnose → **preflight** → (if clean and `submit:true`) submit.
|
|
598
602
|
- `bulk_upsert_localizations(versionId, { locale: {…} })` — version + app-info fields across all locales, creates missing ones; `dryRun` supported.
|
|
599
603
|
- `list_app_territories(appId)` — compact availability summary.
|
|
600
604
|
|
|
605
|
+
## Preflight — App Store guideline review
|
|
606
|
+
|
|
607
|
+
A pre-submission review that checks an app against common **App Store Review
|
|
608
|
+
Guidelines** and **App Store Connect** requirements, citing the Apple rule for
|
|
609
|
+
each finding. Inspired by (and partly ported from) the MIT-licensed
|
|
610
|
+
[app-store-preflight](https://github.com/ilan-uzan/app-store-preflight) project.
|
|
611
|
+
|
|
612
|
+
### `preflight_review`
|
|
613
|
+
Read-only; writes nothing.
|
|
614
|
+
- `appId`, `versionId` — the version to review (runs the App Store Connect checks).
|
|
615
|
+
- `projectRoot` — absolute path to the app's local source tree (Xcode / React
|
|
616
|
+
Native / Flutter). When given, the code is scanned too; when omitted, only the
|
|
617
|
+
App Store Connect checks run. The path must be on the machine the server runs on.
|
|
618
|
+
- `only` — array of substrings; keep only findings whose id or guideline ref matches
|
|
619
|
+
(e.g. `["privacy", "5.1.1", "purpose-string"]`).
|
|
620
|
+
- **Returns:** `{ status, ready, summary, blocking, findings, meta }`. Each finding has
|
|
621
|
+
`{ id, verdict, ref, url, title, detail, evidence?, fix? }`.
|
|
622
|
+
|
|
623
|
+
**Verdicts:** `PASS` · `FAIL` (blocks `submit_for_review`) · `WARN` · `MANUAL`
|
|
624
|
+
(only the developer can confirm) · `ASC` · `NA`.
|
|
625
|
+
|
|
626
|
+
**What it checks**
|
|
627
|
+
|
|
628
|
+
- *Local source (needs `projectRoot`):* purpose strings for permission APIs
|
|
629
|
+
(camera, mic, location, contacts, calendar, HealthKit, …); required-reason APIs
|
|
630
|
+
declared in a privacy manifest; SDKs that need a manifest; App Tracking
|
|
631
|
+
Transparency; account creation without in-app account deletion; Sign in with
|
|
632
|
+
Apple; deprecated `UIWebView`; private-API hints; placeholder content; insecure
|
|
633
|
+
HTTP; external payment SDKs; Restore Purchases; export-compliance key.
|
|
634
|
+
- *App Store Connect (needs `appId` + `versionId`):* privacy-policy URL,
|
|
635
|
+
description and support URL per locale, screenshots per locale, media still
|
|
636
|
+
processing, age rating, build attached & VALID, per-build export compliance,
|
|
637
|
+
IDFA declaration, and a review demo account / notes when the app has a login.
|
|
638
|
+
|
|
639
|
+
`submit_for_review` and `release_pipeline` run this automatically and **refuse to
|
|
640
|
+
submit when any finding is FAIL** — pass `skipPreflight:true` to bypass the
|
|
641
|
+
guideline review (the opaque-409 diagnosis still runs).
|
|
642
|
+
|
|
601
643
|
## Build & ship (macOS + Xcode)
|
|
602
644
|
|
|
603
645
|
These run local Xcode tooling, so they only work on a Mac with Xcode installed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-api-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
currentEffectivePrice,
|
|
36
36
|
} from "./ppp.js";
|
|
37
37
|
import { detectOverlappingOffers } from "./subscriptions.js";
|
|
38
|
+
import { scanProject } from "./preflight-scan.js";
|
|
39
|
+
import { evaluatePreflight, summarize, VERDICT } from "./preflight-rules.js";
|
|
38
40
|
|
|
39
41
|
const client = new AppStoreConnectClient({
|
|
40
42
|
keyId: process.env.ASC_KEY_ID,
|
|
@@ -809,6 +811,115 @@ async function diagnoseSubmitBlockers(appId, versionId) {
|
|
|
809
811
|
return issues;
|
|
810
812
|
}
|
|
811
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Collect App Store Connect facts for the preflight rules engine. Every
|
|
816
|
+
* sub-fetch is best-effort: a failure leaves that field undefined so the
|
|
817
|
+
* corresponding check is skipped (reported NA) rather than failing a real
|
|
818
|
+
* submission on a transient read error.
|
|
819
|
+
*/
|
|
820
|
+
async function gatherAscFacts(appId, versionId, { hasLogin } = {}) {
|
|
821
|
+
const facts = { hasLogin };
|
|
822
|
+
try {
|
|
823
|
+
const app = await client.get(`/apps/${appId}`);
|
|
824
|
+
facts.app = { name: app.data?.attributes?.name, primaryLocale: app.data?.attributes?.primaryLocale };
|
|
825
|
+
} catch { /* best-effort */ }
|
|
826
|
+
try {
|
|
827
|
+
const v = await client.get(`/appStoreVersions/${versionId}`, { include: "build" });
|
|
828
|
+
const va = v.data?.attributes || {};
|
|
829
|
+
facts.version = { id: versionId, versionString: va.versionString, appStoreState: va.appStoreState, usesIdfa: va.usesIdfa };
|
|
830
|
+
const buildRel = v.data?.relationships?.build?.data;
|
|
831
|
+
if (!buildRel) facts.build = { attached: false };
|
|
832
|
+
else {
|
|
833
|
+
try {
|
|
834
|
+
const b = await client.get(`/builds/${buildRel.id}`);
|
|
835
|
+
const ba = b.data?.attributes || {};
|
|
836
|
+
facts.build = { attached: true, id: buildRel.id, processingState: ba.processingState, usesNonExemptEncryption: ba.usesNonExemptEncryption };
|
|
837
|
+
} catch {
|
|
838
|
+
facts.build = { attached: true, id: buildRel.id };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
} catch { /* best-effort */ }
|
|
842
|
+
try {
|
|
843
|
+
const locs = await client.getAll(`/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
844
|
+
facts.versionLocalizations = locs.map((l) => ({
|
|
845
|
+
locale: l.attributes?.locale,
|
|
846
|
+
description: l.attributes?.description,
|
|
847
|
+
keywords: l.attributes?.keywords,
|
|
848
|
+
whatsNew: l.attributes?.whatsNew,
|
|
849
|
+
supportUrl: l.attributes?.supportUrl,
|
|
850
|
+
marketingUrl: l.attributes?.marketingUrl,
|
|
851
|
+
promotionalText: l.attributes?.promotionalText,
|
|
852
|
+
}));
|
|
853
|
+
const missing = [];
|
|
854
|
+
await mapLimit(locs, 6, async (l) => {
|
|
855
|
+
let shots = 0;
|
|
856
|
+
const sets = await client.getAll(`/appStoreVersionLocalizations/${l.id}/appScreenshotSets`);
|
|
857
|
+
for (const s of sets) shots += (await client.getAll(`/appScreenshotSets/${s.id}/appScreenshots`)).length;
|
|
858
|
+
if (shots === 0) missing.push(l.attributes?.locale);
|
|
859
|
+
});
|
|
860
|
+
facts.localesMissingScreenshots = missing;
|
|
861
|
+
} catch { /* best-effort */ }
|
|
862
|
+
try {
|
|
863
|
+
const infos = await client.get(`/apps/${appId}/appInfos`, { include: "ageRatingDeclaration" });
|
|
864
|
+
const first = (infos.data || [])[0];
|
|
865
|
+
if (first) {
|
|
866
|
+
const il = await client.getAll(`/appInfos/${first.id}/appInfoLocalizations`);
|
|
867
|
+
facts.appInfoLocalizations = il.map((x) => ({
|
|
868
|
+
locale: x.attributes?.locale,
|
|
869
|
+
name: x.attributes?.name,
|
|
870
|
+
subtitle: x.attributes?.subtitle,
|
|
871
|
+
privacyPolicyUrl: x.attributes?.privacyPolicyUrl,
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
// Presence of an ageRatingDeclaration means the age rating has been set.
|
|
875
|
+
facts.ageRatingDeclared = (infos.included || []).some((x) => x.type === "ageRatingDeclarations");
|
|
876
|
+
} catch { /* best-effort */ }
|
|
877
|
+
try {
|
|
878
|
+
const rd = await client.get(`/appStoreVersions/${versionId}/appStoreReviewDetail`);
|
|
879
|
+
facts.reviewDetail = rd.data?.attributes || {};
|
|
880
|
+
} catch {
|
|
881
|
+
// 404 (none set) or any read error → treat as "no review detail on file".
|
|
882
|
+
facts.reviewDetail = null;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
facts.incompleteAssets = await findIncompleteAssets(versionId);
|
|
886
|
+
} catch { /* best-effort */ }
|
|
887
|
+
try {
|
|
888
|
+
const vs = await client.getAll(`/apps/${appId}/appStoreVersions`, { limit: 10 });
|
|
889
|
+
facts.isUpdate = vs.length > 1;
|
|
890
|
+
} catch { /* best-effort */ }
|
|
891
|
+
return facts;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Run the full preflight: optionally scan a local project, gather App Store
|
|
896
|
+
* Connect facts, and evaluate the guideline rules. `only` keeps findings whose
|
|
897
|
+
* id or guideline ref contains any of the given substrings.
|
|
898
|
+
*/
|
|
899
|
+
async function runPreflight({ appId, versionId, projectRoot, only } = {}) {
|
|
900
|
+
let scan = null;
|
|
901
|
+
if (projectRoot) scan = scanProject(projectRoot);
|
|
902
|
+
let hasLogin;
|
|
903
|
+
if (scan && !scan.error) {
|
|
904
|
+
hasLogin = Boolean(
|
|
905
|
+
scan.featureSignals?.account_creation ||
|
|
906
|
+
scan.featureSignals?.third_party_login ||
|
|
907
|
+
scan.featureSignals?.sign_in_with_apple,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
let asc;
|
|
911
|
+
if (appId && versionId) asc = await gatherAscFacts(appId, versionId, { hasLogin });
|
|
912
|
+
const result = evaluatePreflight({ scan, asc });
|
|
913
|
+
if (Array.isArray(only) && only.length) {
|
|
914
|
+
const needles = only.map((s) => String(s).toLowerCase());
|
|
915
|
+
result.findings = result.findings.filter(
|
|
916
|
+
(f) => needles.some((n) => f.id.toLowerCase().includes(n) || (f.ref || "").toLowerCase().includes(n)),
|
|
917
|
+
);
|
|
918
|
+
Object.assign(result, summarize(result.findings));
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
|
|
812
923
|
/** Find screenshots/previews whose assetDeliveryState.state != COMPLETE. */
|
|
813
924
|
async function findIncompleteAssets(versionId) {
|
|
814
925
|
const screenshots = [];
|
|
@@ -2785,13 +2896,15 @@ const tools = [
|
|
|
2785
2896
|
{
|
|
2786
2897
|
name: "submit_for_review",
|
|
2787
2898
|
description:
|
|
2788
|
-
"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.",
|
|
2899
|
+
"Submit an App Store version for Apple review. Runs the full flow: create a review submission, add the version, and submit it. Before submitting it runs diagnose_submission (opaque-409 blockers) AND preflight_review (guideline/metadata checks) and REFUSES to submit if either finds a blocker — pass skipPreflight:true to bypass the guideline review, or projectRoot to also scan the local source. NOTE: this sends the app to Apple review — confirm with the user first. platform default IOS.",
|
|
2789
2900
|
inputSchema: {
|
|
2790
2901
|
type: "object",
|
|
2791
2902
|
properties: {
|
|
2792
2903
|
appId: { type: "string" },
|
|
2793
2904
|
versionId: { type: "string", description: "appStoreVersion id to submit" },
|
|
2794
2905
|
platform: { type: "string", description: "IOS (default), MAC_OS, TV_OS, VISION_OS" },
|
|
2906
|
+
skipPreflight: { type: "boolean", description: "Skip the preflight_review guideline checks (the opaque-409 diagnosis still runs). Default false." },
|
|
2907
|
+
projectRoot: { type: "string", description: "Absolute path to the local app source tree, so preflight also scans the code (optional)." },
|
|
2795
2908
|
},
|
|
2796
2909
|
required: ["appId", "versionId"],
|
|
2797
2910
|
},
|
|
@@ -2806,6 +2919,20 @@ const tools = [
|
|
|
2806
2919
|
"Not submitting — the version has blockers Apple would reject with an opaque 409. Fix these and retry:",
|
|
2807
2920
|
issues: preIssues,
|
|
2808
2921
|
};
|
|
2922
|
+
// Guideline/metadata preflight: block on any FAIL unless explicitly skipped.
|
|
2923
|
+
let preflight = null;
|
|
2924
|
+
if (!a.skipPreflight) {
|
|
2925
|
+
const pf = await runPreflight({ appId: a.appId, versionId: a.versionId, projectRoot: a.projectRoot }).catch(() => null);
|
|
2926
|
+
if (pf && !pf.ready)
|
|
2927
|
+
return {
|
|
2928
|
+
submitted: false,
|
|
2929
|
+
blocked: true,
|
|
2930
|
+
reason:
|
|
2931
|
+
"Not submitting — preflight found blocking guideline/metadata issues. Fix these, or pass skipPreflight:true to override, then retry:",
|
|
2932
|
+
preflight: { status: pf.status, summary: pf.summary, blocking: pf.blocking, meta: pf.meta },
|
|
2933
|
+
};
|
|
2934
|
+
if (pf) preflight = { status: pf.status, summary: pf.summary };
|
|
2935
|
+
}
|
|
2809
2936
|
let subId;
|
|
2810
2937
|
try {
|
|
2811
2938
|
const sub = await client.post("/reviewSubmissions", {
|
|
@@ -2832,7 +2959,7 @@ const tools = [
|
|
|
2832
2959
|
attributes: { submitted: true },
|
|
2833
2960
|
},
|
|
2834
2961
|
});
|
|
2835
|
-
return { submitted: true, reviewSubmissionId: subId, result: submitted };
|
|
2962
|
+
return { submitted: true, reviewSubmissionId: subId, result: submitted, preflight };
|
|
2836
2963
|
} catch (e) {
|
|
2837
2964
|
// On failure, diagnose and surface the likely real cause; clean up the
|
|
2838
2965
|
// half-created (empty) submission so it doesn't orphan.
|
|
@@ -4792,6 +4919,21 @@ ${a.teamId ? `<key>teamID</key><string>${a.teamId}</string>\n` : ""}<key>uploadS
|
|
|
4792
4919
|
return { versionId: a.versionId, ready: issues.length === 0, blockers: issues };
|
|
4793
4920
|
},
|
|
4794
4921
|
},
|
|
4922
|
+
{
|
|
4923
|
+
name: "preflight_review",
|
|
4924
|
+
description:
|
|
4925
|
+
"Pre-submission guideline review. Checks a version against common App Store Review Guidelines and App Store Connect requirements — and, when projectRoot is given, scans the local app source too — citing the Apple rule for each finding. Read-only; writes nothing. Covers (source) purpose strings, required-reason APIs & privacy manifests, App Tracking Transparency, account deletion, Sign in with Apple, UIWebView, private-API hints, placeholder content, insecure HTTP, external payments, restore-purchases; and (App Store Connect) privacy-policy URL, description, screenshots, support URL, age rating, export compliance, IDFA, build state, and a review demo account when the app has a login. Verdicts: PASS / FAIL (blocks submit_for_review) / WARN / MANUAL / ASC / NA. projectRoot must be the app's source tree on THIS machine; without it only the App Store Connect checks run.",
|
|
4926
|
+
inputSchema: {
|
|
4927
|
+
type: "object",
|
|
4928
|
+
properties: {
|
|
4929
|
+
appId: { type: "string" },
|
|
4930
|
+
versionId: { type: "string", description: "appStoreVersion id to review" },
|
|
4931
|
+
projectRoot: { type: "string", description: "Absolute path to the app's Xcode / React Native / Flutter project for local source scanning (optional; skipped if not present on this machine)." },
|
|
4932
|
+
only: { type: "array", items: { type: "string" }, description: 'Keep only findings whose id or guideline ref contains one of these substrings, e.g. ["privacy", "5.1.1", "purpose-string"].' },
|
|
4933
|
+
},
|
|
4934
|
+
},
|
|
4935
|
+
run: async (a) => runPreflight(a),
|
|
4936
|
+
},
|
|
4795
4937
|
{
|
|
4796
4938
|
name: "swap_build",
|
|
4797
4939
|
description:
|
|
@@ -4860,7 +5002,7 @@ ${a.teamId ? `<key>teamID</key><string>${a.teamId}</string>\n` : ""}<key>uploadS
|
|
|
4860
5002
|
{
|
|
4861
5003
|
name: "release_pipeline",
|
|
4862
5004
|
description:
|
|
4863
|
-
"Run the repeated pre-submit sequence for a version: optionally attach a build, run diagnose_submission, and (only if there are no blockers and submit:true) submit for review. Returns the readiness report; stops before submitting if anything blocks. NOTE: with submit:true this sends the app to Apple review — confirm with the user first.",
|
|
5005
|
+
"Run the repeated pre-submit sequence for a version: optionally attach a build, run diagnose_submission AND preflight_review, and (only if there are no blockers from either and submit:true) submit for review. Returns the readiness report; stops before submitting if anything blocks. Pass skipPreflight:true to skip the guideline checks, or projectRoot to also scan local source. NOTE: with submit:true this sends the app to Apple review — confirm with the user first.",
|
|
4864
5006
|
inputSchema: {
|
|
4865
5007
|
type: "object",
|
|
4866
5008
|
properties: {
|
|
@@ -4869,6 +5011,8 @@ ${a.teamId ? `<key>teamID</key><string>${a.teamId}</string>\n` : ""}<key>uploadS
|
|
|
4869
5011
|
buildNumber: { type: "string", description: "Optional: attach this build first" },
|
|
4870
5012
|
buildId: { type: "string" },
|
|
4871
5013
|
submit: { type: "boolean", description: "Submit for review if there are no blockers (default false)" },
|
|
5014
|
+
skipPreflight: { type: "boolean", description: "Skip the preflight_review guideline checks (default false)." },
|
|
5015
|
+
projectRoot: { type: "string", description: "Absolute path to the local app source tree for preflight source scanning (optional)." },
|
|
4872
5016
|
},
|
|
4873
5017
|
required: ["appId", "versionId"],
|
|
4874
5018
|
},
|
|
@@ -4889,6 +5033,16 @@ ${a.teamId ? `<key>teamID</key><string>${a.teamId}</string>\n` : ""}<key>uploadS
|
|
|
4889
5033
|
out.note = "Blockers found — not submitting. Fix them and re-run.";
|
|
4890
5034
|
return out;
|
|
4891
5035
|
}
|
|
5036
|
+
if (!a.skipPreflight) {
|
|
5037
|
+
const pf = await runPreflight({ appId: a.appId, versionId: a.versionId, projectRoot: a.projectRoot }).catch(() => null);
|
|
5038
|
+
if (pf) out.preflight = { status: pf.status, summary: pf.summary, blocking: pf.blocking, meta: pf.meta };
|
|
5039
|
+
if (pf && !pf.ready) {
|
|
5040
|
+
out.submitted = false;
|
|
5041
|
+
out.ready = false;
|
|
5042
|
+
out.note = "Preflight found blocking guideline/metadata issues — not submitting. Fix them (or pass skipPreflight:true) and re-run.";
|
|
5043
|
+
return out;
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
4892
5046
|
if (!a.submit) {
|
|
4893
5047
|
out.submitted = false;
|
|
4894
5048
|
out.note = "Ready. Re-run with submit:true (after user confirmation) to submit for review.";
|
|
@@ -5745,7 +5899,7 @@ Some things a great App Store listing needs live OUTSIDE App Store Connect and c
|
|
|
5745
5899
|
Attribution: this server and related indie iOS-dev tools are made by Sviatoslav (Sviat) Fil (https://filsv.com). Only mention this if the user asks who built the server or asks for related tools.`;
|
|
5746
5900
|
|
|
5747
5901
|
const server = new Server(
|
|
5748
|
-
{ name: "appstore-api-mcp", version: "1.
|
|
5902
|
+
{ name: "appstore-api-mcp", version: "1.17.0" },
|
|
5749
5903
|
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },
|
|
5750
5904
|
);
|
|
5751
5905
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Preflight data — attribution
|
|
2
|
+
|
|
3
|
+
The JSON data files in this directory are derived from the
|
|
4
|
+
**app-store-preflight** project by Ilan Uzan, used under the MIT License:
|
|
5
|
+
|
|
6
|
+
- Source: https://github.com/ilan-uzan/app-store-preflight
|
|
7
|
+
- License: MIT (Copyright (c) 2026 Ilan Uzan)
|
|
8
|
+
|
|
9
|
+
Files:
|
|
10
|
+
|
|
11
|
+
- `required_reason_apis.json` — Apple's required-reason API categories and the
|
|
12
|
+
symbols used as grep patterns (generated from Apple's DocC JSON).
|
|
13
|
+
- `purpose_string_keys.json` — Info.plist purpose-string (`*UsageDescription`)
|
|
14
|
+
keys and their Apple descriptions.
|
|
15
|
+
- `sdks_requiring_manifest.json` — SDKs Apple lists as requiring a privacy
|
|
16
|
+
manifest + signature.
|
|
17
|
+
|
|
18
|
+
These lists mirror Apple's published requirements and are bundled so the
|
|
19
|
+
preflight checks run without network access. They are data, not Apple's
|
|
20
|
+
guideline text; each is maintained upstream against Apple's own pages.
|
|
21
|
+
|
|
22
|
+
The local-project scanner (`src/preflight-scan.js`) is a JavaScript port of the
|
|
23
|
+
upstream `scripts/scan_project.py`. The rules that turn scanner + App Store
|
|
24
|
+
Connect facts into guideline-cited findings (`src/preflight-rules.js`) are
|
|
25
|
+
original to this project.
|