Skip to main content
Glama
README.md
<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.

[![CI](https://github.com/menansali/ios-ship-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/menansali/ios-ship-doctor/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](./LICENSE)
[![Node](https://img.shields.io/badge/node-%E2%89%A518-black.svg)](https://nodejs.org)
[![MCP](https://img.shields.io/badge/protocol-MCP-black.svg)](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

A4.3/5.0

Scored across 6 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues