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 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
- **Sends the app to review — confirm first.**
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.16.4",
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.16.4" },
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.