ios-ship-doctor
<div align="center">
# 🩺 iOS Ship Doctor
**Catch App Store rejections *before* you hit Submit.**
An [MCP](https://modelcontextprotocol.io) server that lets your AI assistant — Claude Code, Gemini CLI, Codex, Cursor, Copilot, Windsurf, Zed — diagnose why your iOS app will be rejected — privacy manifests, missing permission strings, unlisted SDKs, leftover test credentials — and then fix them. When Apple *does* reject you, it pulls the rejection and maps it to a plain-English fix.
[](https://github.com/menansali/ios-ship-doctor/actions/workflows/ci.yml)
[](./LICENSE)
[](https://nodejs.org)
[](https://modelcontextprotocol.io)
<br/>
<img src="./assets/demo.svg" alt="Ship Doctor preflight output" width="720"/>
</div>
---
## Why
Apple's review pipeline silently rejects builds for things you can't see in Xcode:
- A required-reason API used in code but **not declared** in `PrivacyInfo.xcprivacy`
- A permission used with **no `NS…UsageDescription`** string (instant crash + reject)
- A third-party SDK on Apple's list shipping **without its own privacy manifest**
- A **placeholder test credential** (like Google's sample AdMob ID) left in `Info.plist`
Each one costs you a submission cycle — hours or days of round-trips. Ship Doctor checks all of them in seconds and tells you exactly what to change.
> Existing App Store MCPs are thin API wrappers that *submit* your app. Ship Doctor is the one that tells you **why it won't pass** first.
## Quickstart
No install needed — `npx` fetches and runs it:
```bash
npx -y ios-ship-doctor-mcp preflight /path/to/your/app
```
Then wire it into whichever assistant you use. This is a standard stdio MCP server — it is **not** Claude-specific and works with any MCP client. Let it print the exact config for yours:
```bash
npx -y ios-ship-doctor-mcp config # every client
npx -y ios-ship-doctor-mcp config gemini # just one
```
<details>
<summary>Or clone and build from source</summary>
```bash
git clone https://github.com/menansali/ios-ship-doctor.git
cd ios-ship-doctor
npm install && npm run build
npm link # exposes the `ios-ship-doctor-mcp` command
```
</details>
Supported out of the box: `claude`, `gemini`, `codex`, `cursor`, `vscode`, `windsurf`, `zed`, `generic`.
| Client | Config file | Root key |
|---|---|---|
| Claude Code | `claude mcp add ios-ship-doctor -- npx -y ios-ship-doctor-mcp` | `mcpServers` |
| Gemini CLI | `~/.gemini/settings.json` (or `gemini mcp add …`) | `mcpServers` |
| OpenAI Codex CLI | `~/.codex/config.toml` | `[mcp_servers.…]` ⚠️ underscore |
| Cursor | `~/.cursor/mcp.json` or `.cursor/mcp.json` | `mcpServers` |
| VS Code (Copilot) | `.vscode/mcp.json` | `servers` ⚠️ not `mcpServers` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` |
| Zed | Zed `settings.json` | `context_servers` ⚠️ `command.path` |
| Anything else | — | `mcpServers` |
The two footguns the generator handles for you: Codex silently ignores `mcp-servers` (it must be `mcp_servers`), and the printed `node` path avoids version-pinned Homebrew/nvm paths that break on the next upgrade.
Then just ask:
> **Is `/path/to/my-app` ready to submit to the App Store?**
## Example
```
🩺 Ship Doctor preflight — /Users/you/Desktop/MyApp
VERDICT: NOT READY — 1 issue that commonly causes rejection.
(1 error, 1 warning, 6 passed)
❌ [credential-traps] Google AdMob TEST application ID in Info.plist
Found placeholder/test value "ca-app-pub-3940256099942544".
↳ MyApp/Info.plist
💡 Replace GADApplicationIdentifier with your real ca-app-pub-… ID.
⚠️ [export-compliance] Missing ITSAppUsesNonExemptEncryption in Info.plist
Without this key, App Store Connect prompts about encryption on every submission.
đź’ˇ Add <key>ITSAppUsesNonExemptEncryption</key><false/> if you only use HTTPS/TLS.
âś… [privacy-manifest] Privacy manifest consistent with API usage
âś… [usage-descriptions] All permission strings present
âś… [dependencies] SDK privacy manifests OK
âś… [app-icon] App icon asset found
```
## Tools
### Preflight — local, no credentials
| Tool | What it catches |
|------|-----------------|
| `preflight` | Runs every check below → single **READY / NOT READY** verdict |
| `scan_privacy_manifest` | Required-reason APIs used but undeclared, invalid reason codes, missing manifest |
| `check_usage_descriptions` | Camera / location / photos / mic / tracking used with no `NS…UsageDescription` |
| `audit_dependencies` | Apple-listed SDKs shipping without a `PrivacyInfo.xcprivacy` |
| `check_credential_traps` | Placeholder / public test credentials in `Info.plist` |
| `check_legal_links` | Missing Privacy Policy / Terms of Use (EULA) links on a paywall + in the App Store description (3.1.2) |
| `check_account_requirements` | No demo account for App Review (2.1), missing in-app account deletion (5.1.1(v)), social login without Sign in with Apple (4.8) |
| `check_external_payments` | Stripe/PayPal/Paddle for digital content with no StoreKit (3.1.1) |
| `check_background_modes` | `UIBackgroundModes` entries the app never actually implements (2.5.4) |
| `check_placeholder_content` | Lorem ipsum, Stripe test keys, `YOUR_API_KEY`, example.com dead links, template app names (2.1) |
| `generate_privacy_manifest` | Writes a valid `PrivacyInfo.xcprivacy` covering every detected API |
| `autofix` | Applies the safe fixes automatically; reports the rest as manual follow-up |
`preflight` also checks **export compliance**, **App Transport Security**, **app-icon presence**, **banned APIs** (`UIWebView`), **launch screen**, **version/build sanity**, and **deployment target**. Dependency scanning covers both **CocoaPods and Swift Package Manager**.
Project layouts understood: classic `Info.plist` projects, **modern projects with `GENERATE_INFOPLIST_FILE` and no plist file** (keys read from `INFOPLIST_KEY_*` build settings), `ios/` subdirectories, and monorepos where the `.xcodeproj` sits a level or two down.
### What this does *not* check
Ship Doctor reads project files. It cannot see the things most rejections are actually about:
- **Crashes and incomplete features (2.1)** — the most common rejection; needs the app running
- **Design quality (4.0)** and **spam/duplicate (4.3)** — human judgement
- **Metadata accuracy (2.3)** — whether screenshots match the real app
- **Privacy label accuracy (5.1.1)** — whether declared data collection matches SDK behaviour
- Anything a **dependency** does; source scanning is first-party only
A clean run means "no automated check fired", not "this will be approved."
### Rejection recovery — needs an App Store Connect API key
| Tool | What it does |
|------|--------------|
| `asc_list_apps` | Lists your apps (id, name, bundle id) |
| `asc_get_rejections` | Pulls recent rejections, maps Review Guideline numbers → summaries + fixes |
| `asc_check_submission` | The metadata half of preflight: demo credentials actually filled in, review notes, required screenshot sets |
| `explain_guideline` | Explains any Review Guideline number (offline, no key needed) |
<details>
<summary><b>Enabling rejection recovery</b></summary>
Create a key at **App Store Connect → Users and Access → Integrations → App Store Connect API**, download the `.p8`, and add env vars to the MCP config:
```json
{
"mcpServers": {
"ios-ship-doctor": {
"command": "ios-ship-doctor-mcp",
"env": {
"ASC_KEY_ID": "XXXXXXXXXX",
"ASC_ISSUER_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ASC_PRIVATE_KEY_PATH": "/absolute/path/to/AuthKey_XXXXXXXXXX.p8"
}
}
}
}
```
The `.p8` never enters the repo — it stays on your machine and is read at runtime.
</details>
## Manual configuration
If you'd rather not use `npm link`, point your client at the built file directly (this shape works for Claude Code, Gemini CLI, Cursor, Windsurf and most others):
```json
{
"mcpServers": {
"ios-ship-doctor": {
"command": "node",
"args": ["/absolute/path/to/ios-ship-doctor/dist/index.js"]
}
}
}
```
`projectPath` in any tool can be your repo root (with an `ios/` folder) or the `ios/` directory itself.
## Use it in CI (no assistant required)
The same binary runs as a one-shot command that exits non-zero on blocking issues:
```bash
npx -y ios-ship-doctor-mcp preflight /path/to/app # human-readable
npx -y ios-ship-doctor-mcp preflight /path/to/app --json # machine-readable
```
Copy [`examples/preflight-gate.yml`](./examples/preflight-gate.yml) into your app repo's `.github/workflows/` to **block PRs that would fail App Store review**.
## How it works
- **Required-reason APIs** — scans first-party source for Apple's documented API signatures, then diffs against your declared `PrivacyInfo.xcprivacy`.
- **App Store Connect auth** — an ES256 JWT signed with Node's built-in `crypto` (no `jsonwebtoken` dependency), in the JOSE (IEEE-P1363) format Apple requires.
- **Safe by default** — everything is read-only except `generate_privacy_manifest` with `write=true`.
## Roadmap
- [x] Auto-fix tools (patch `Info.plist`, generate privacy manifest)
- [x] More preflight checks: launch screen, version sanity, deployment target
- [x] SwiftPM dependency scanning
- [x] CLI + CI mode
- [ ] Entitlements sanity (push, app groups, associated domains)
- [ ] Privacy nutrition-label cross-check against SDK data collection
- [ ] Draft reviewer replies from a rejection
## Contributing
Issues and PRs welcome. Adding a check usually means one entry in `src/knowledge.ts` plus a small function in `src/scanner.ts`.
## License
[MIT](./LICENSE) © menansali
TDQS
Scored across 6 tools
Each tool serves a distinct, non-overlapping purpose: ASC readiness, app listing, placeholder scanning, guideline explanation, privacy manifest generation, and a comprehensive preflight. No two tools could be confused.
Most tools follow a verb_noun pattern (e.g., check_placeholder_content, generate_privacy_manifest) with a consistent snake_case style. The only outlier is 'preflight' which is a single word, but it is a common term and the overall pattern is clear.
With exactly 6 tools, the server covers the core aspects of iOS app submission readiness without being excessive or lacking. Each tool justifies its presence.
The server covers the full lifecycle of pre-submission checks: placeholder content, privacy manifest generation, Info.plist keys, third-party SDK manifests, ASC credentials and metadata, and a comprehensive preflight that ties it all together. There are no obvious gaps for its stated purpose.