Skip to main content
Glama
store-preflight-mcp

store-preflight-mcp

by asif786ka

store-preflight-mcp

PyPI Python License: MIT Tests

Your agent just wrote code that will get your app rejected. It has no idea.

An MCP server that maps API usage โ†’ required declaration โ†’ dated policy, so a coding agent finds out at write time instead of from an App Store Connect email three days after the PR merged.

$ store-preflight scan . --date 2026-07-15

๐Ÿ›‘ UserDefaults requires a declared reason in PrivacyInfo.xcprivacy
    ITMS-91053
    No PrivacyInfo.xcprivacy exists anywhere in this project. Code touches
    UserDefaults APIs, so App Store Connect will refuse the upload.
      โ†’ ios/RejectedApp/AnalyticsModule.swift:7  private let defaults = UserDefaults.standard
    fix: PrivacyInfo.xcprivacy โ†’ NSPrivacyAccessedAPICategoryUserDefaults

โœ— failing: 7 finding(s) at or above blocker

That's the whole product. An agent adds an analytics module, touches UserDefaults, and nothing in the world tells it that Apple has required a CA92.1 entry in a file it has never heard of since 1 May 2024. The binary is rejected days later, by which point nobody remembers which PR did it.

Doc MCPs describe APIs. None of them map an API to the declaration it obliges, or to the date that obligation acquired teeth.


Why dates are the whole design

This is the sibling of mobile-docs-mcp, and it reuses its core insight. There, a symbol isn't "real" โ€” it's real within a version range (since / deprecated / removed). Here, a rule isn't "true" โ€” it's true within a date window:

PolicyWindow(
    announced     = "2023-06-05",   # WWDC23
    warning_from  = "2024-03-13",   # email only, upload still succeeds
    enforced_from = "2024-05-01",   # hard block
)

PolicyWindow.status_on(date) is the direct analogue of VersionRange.status_at(version). Same model, different axis โ€” because store policy ships on deadlines, not releases.

This isn't decoration. It's what makes the tool usable:

  • Severity follows the calendar. A blocker that doesn't bite until October is reported as a warning in July, automatically. Nobody hand-edits severities as deadlines pass.

  • You can scan the future. --date 2026-10-28 tells you what breaks when a deadline lands, before it lands.

  • The tests don't rot. Every assertion is pinned to a fixed date, so the suite doesn't silently change verdict when a deadline passes.

The same unchanged repo, scanned on two dates:

$ store-preflight scan . --date 2026-07-15
โ„น๏ธ  Geofencing is no longer an approved use case for a location foreground service
โš ๏ธ  READ_CONTACTS requires a declaration for apps targeting Android 17+

$ store-preflight scan . --date 2026-10-28
โš ๏ธ  Geofencing is no longer an approved use case for a location foreground service
๐Ÿ›‘ READ_CONTACTS requires a declaration for apps targeting Android 17+

Not one byte of that repo changed. That failure mode is invisible to every other tool in the toolchain โ€” a linter diffs your code, and your code is fine.

(Why does READ_CONTACTS escalate to a blocker while geofencing stops at a warning? Because the permission string is either in your manifest or it isn't, whereas whether geofencing is what justifies your location FGS depends on a form you filed in Play Console, which isn't in the repo. See On being trustworthy.)


Related MCP server: @squirex.dev/mcp-server

Install

uvx store-preflight-mcp          # MCP server (stdio)
pipx install store-preflight-mcp # CLI too

Claude Code / any MCP client:

{
  "mcpServers": {
    "store-preflight": {
      "command": "uvx",
      "args": ["store-preflight-mcp"]
    }
  }
}

Zero non-mcp runtime dependencies. No crawl, no API key, no network. The ruleset ships as Python and the whole scan is local.


Tools

Tool

Use

preflight_scan

Scan a repo. What will be rejected, where, and why.

check_api_declaration

Before writing: "I'm about to use UserDefaults โ€” what must I declare?"

get_required_declaration

The exact key + reason code + paste-ready snippet.

explain_rejection

Paste ITMS-91053 from the email โ†’ a concrete fix.

list_policy_deadlines

The dated calendar. What's enforced, what lands next.

check_api_declaration is the one that changes agent behaviour โ€” it's cheap enough to call before writing the code, which is the only time the answer is free.

> check_api_declaration("UserDefaults", store="apple")

๐Ÿ›‘ BLOCKER ยท status enforced โ€” enforced since 2024-05-01 (806 days ago)

You must declare: NSPrivacyAccessedAPICategoryUserDefaults in PrivacyInfo.xcprivacy

Valid values:
  ยท CA92.1 โ€” Read/write info accessible only to the app itself...
  ยท 1C8F.1 โ€” ...only to apps, extensions and App Clips in the same App Group...
  ยท C56D.1 โ€” Third-party SDK only: wrapper function around UserDefaults...
  ยท AC6B.1 โ€” Read com.apple.configuration.managed (MDM managed app config)...

CI

- run: pipx install store-preflight-mcp
- run: store-preflight scan . --format sarif -o preflight.sarif
- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: preflight.sarif }

SARIF means findings render as inline PR annotations, on the line that caused them. --fail-on blocker (the default) exits non-zero, so a rejection becomes a red check instead of an email.


Coverage

App Store

  • All 5 required-reason API categories, all 17 reason codes, each with Apple's verbatim meaning โ€” UserDefaults, FileTimestamp, SystemBootTime, DiskSpace, ActiveKeyboards

  • 17 Info.plist purpose strings, incl. the iOS 17 calendar split (NSCalendarsFullAccessUsageDescription) most projects still miss

  • Privacy manifest validity: ITMS-91054 / 91055 / 91056

  • All 86 privacy-impacting SDKs + npm/pub โ†’ framework aliasing

  • NSPrivacyTracking without NSPrivacyTrackingDomains

  • The Xcode 26 / iOS 26 SDK upload gate (since 2026-04-28)

  • All 6 ITMS rejection codes decoded

Google Play

  • Restricted permissions: QUERY_ALL_PACKAGES, MANAGE_EXTERNAL_STORAGE, SMS/Call Log, ACCESS_BACKGROUND_LOCATION, REQUEST_INSTALL_PACKAGES, USE_EXACT_ALARM, USE_FULL_SCREEN_INTENT, accessibility, photo/video

  • 14 data safety categories with their data types

  • All 14 foreground service types (including mediaProcessing, which most lists miss), each with its required permission and runtime prerequisite

  • Advertising ID, including the auto-merged-from-play-services-ads trap

  • New for 2026: the Contacts policy (2026-10-28) and the geofencing withdrawal (2026-08-26)

Languages: Swift, Objective-C, Kotlin, Java, JS/TS, Dart โ€” plus Info.plist, PrivacyInfo.xcprivacy, AndroidManifest.xml, package.json, pubspec.yaml, Podfile. React Native, Flutter and native all work; no build required.


On being trustworthy

A preflight tool that cries wolf gets --ignored within a week, and then it may as well not exist. Three deliberate concessions:

Findings carry their own epistemics. Apple's required-reason rules are mechanically checkable: the symbol is in your binary or it isn't. Play's data safety form is not โ€” it keys on transmission off-device, not on holding a permission. Google says plainly that on-device processing needs no disclosure. So READ_CONTACTS does not imply a Contacts disclosure, and every such mapping is marked heuristic: it raises a question, and a heuristic finding can never fail your build (enforced by a test).

The target API level is not hardcoded. When this ruleset was built (2026-07-15), Google's own timeline page was internally inconsistent โ€” metadata dated May 2026, content still describing the August 2025 / API 35 deadline โ€” and no API 36 announcement existed on any Google property, despite third-party blogs confidently asserting one. So the rule is encoded ("within one year of the latest major Android release") along with last_verified and a needs-live-check flag. A tool that states a fake deadline confidently is worse than one that says "verify this".

The SDK check is a prompt, not a verdict. Apple's requirement attaches when an app is new or an update adds a listed SDK โ€” not to every app that contains Firebase. And "any SDKs that repackage those on the list" are included, so exact matching under-detects. Both facts make a confident verdict impossible, so it ships as a warning that says why.

What we do not concede: comments and string literals are stripped before symbol matching (with line numbers preserved), node_modules/Pods are never walked, and tools:node="remove" is honoured โ€” so the correct way to drop the auto-merged AD_ID permission isn't reported as the violation.


Verify it

python smoke_test.py

70 assertions, no network. Four fixture repos: one that gets rejected, the same app fixed, a manifest that is wrong on its own terms, and one whose code never changes while the policy date moves under it. The suite also pins the facts that are easy to get wrong:

โœ“ exactly 5 required-reason categories
โœ“ exactly 17 reason codes
โœ“ 86 SDKs on the commonly-used list
โœ“ getattrlist maps to BOTH FileTimestamp and DiskSpace (not 1:1)
โœ“ ITMS-91054 title is 'Invalid API category declaration'
โœ“ 14 foreground service types incl. mediaProcessing
โœ“ target SDK is a rule, not a hardcoded literal
โœ“ no heuristic finding is ever a blocker

Those aren't padding โ€” each one is a mistake that was actually made and caught during the build. getattrlist really does oblige two separate declarations. The SDK list really is 86, not 87, because Apple publishes "BoringSSL / openssl_grpc" as one entry.

See RULES.md for provenance โ€” every rule's source, and the 18 traps that make this domain hostile to naive implementations (Apple's docs are JS-rendered shells; there's a JSON feed that isn't).

Licence

MIT

Available Tools

5 tools
check_api_declarationA

Check what an API, permission or SDK obliges you to declare โ€” BEFORE writing code.

This is the pre-write counterpart to preflight_scan. Ask it before you add UserDefaults, android.permission.READ_SMS, or a new SDK, and you will not find out from a rejection email days later.

Args: api: symbol, permission, or dependency, e.g. "UserDefaults", "NSPrivacyAccessedAPICategoryDiskSpace", "READ_SMS", "shared_preferences", "mach_absolute_time", "foregroundServiceType=dataSync". store: "apple" | "play" | "" for both. scan_date: ISO date for policy-window evaluation (default today).

Inputs: api (required), store, scan_date. Returns: result

explain_rejectionA

Decode an App Store rejection code (ITMS-91053 etc) into a concrete fix.

Paste the code from the App Store Connect email.

Args: code: e.g. "ITMS-91053", or the full email text โ€” the code is extracted.

Inputs: code (required). Returns: result

get_required_declarationA

Get the exact declaration and a paste-ready snippet for a rule.

Args: rule_id: e.g. "apple.rr.user_defaults" (from preflight_scan output). reason_code: for Apple required-reason rules, tailor the snippet to this code (e.g. "CA92.1"). Defaults to the rule's most common choice.

Inputs: rule_id (required), reason_code. Returns: result

list_policy_deadlinesA

Show the dated policy calendar โ€” what is enforced, what lands next.

Store policy ships on deadlines, not releases, so "is this required?" only has an answer relative to a date.

Args: store: "apple" | "play" | "" for both. scan_date: ISO date to evaluate against (default today). upcoming_only: only show requirements not yet enforced.

Inputs: store, scan_date, upcoming_only. Returns: result

preflight_scanA

Scan a mobile app repo for App Store / Play rejections BEFORE submission.

Detects API usage (Swift/Obj-C/Kotlin/Java/JS/TS/Dart) and dependencies, reads the project's Info.plist / PrivacyInfo.xcprivacy / AndroidManifest.xml, and reports every declaration the code obliges but the project does not make.

Args: path: repo root to scan. store: "apple" | "play" | "" for both. scan_date: ISO date to evaluate policy windows against (default today). Policy is dated, so the same repo yields different verdicts on different dates โ€” pass a future date to preview an upcoming deadline. severity: minimum to report โ€” "blocker" | "warning" | "info". include_snippets: append paste-ready fix snippets.

Inputs: path (required), store, severity, scan_date, +1 more. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelogโ€ฆ

    TDQS

    A4.1/5.0

    Scored across 5 tools

    Disambiguation4/5

    Most tools have distinct purposes: scanning, explaining rejections, and listing deadlines. However, check_api_declaration and get_required_declaration both retrieve declaration information from different inputs (API symbol vs rule_id), which could cause confusion for an agent trying to select the right one.

    Naming Consistency4/5

    Four tools follow a clear verb_noun pattern (check_api_declaration, explain_rejection, get_required_declaration, list_policy_deadlines). preflight_scan is a slight deviation as a compound noun, but it still clearly conveys its purpose.

    Tool Count5/5

    With five tools, the server covers the essential preflight workflow without extraneous tools. Each tool serves a distinct role in the submission preparation process, and the count feels well-scoped.

    Completeness4/5

    The tool surface covers the core workflow: scanning, declaration lookup, rejection interpretation, and policy deadlines. A minor gap is the lack of a tool to compare scan results or generate reports, but the set is sufficient for the primary use case.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers