{
  "markdown": "# StorePilot\n\n[![CI](https://github.com/sonlenef/storepilot-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sonlenef/storepilot-mcp/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)\n\nOne MCP server for a whole app portfolio, across Google Play and the App Store.\n\nAsk your assistant:\n\n> \"Which of my apps has a crash rate above Google's threshold?\"\n>\n> \"How much did the portfolio earn last month, per app?\"\n>\n> \"Show me every app on both stores: version, rating, installs, revenue, crashes.\"\n>\n> \"The German description drifted between the two stores — what's different?\"\n>\n> \"Ship 3.2.1 to the Play internal track and TestFlight.\"\n\nEach question above is one tool call covering every app you own, because the\ntools are portfolio-shaped rather than endpoint-shaped. The last one is two, on\npurpose: writes preview first and wait for a human.\n\nYou need your own developer accounts — a Google Play service account, an App\nStore Connect API key, or both. StorePilot reads your accounts; it does not\nscrape public store listings.\n\n> **Status: early.** The read tools have run against real accounts on both\n> stores; the money and crash-threshold paths have not returned real data yet,\n> and no write has ever executed against a store. Read\n> [Status and honest limits](#status-and-honest-limits) before trusting a number.\n\n---\n\n## What is actually different\n\n**Both stores, one tool set.** Google Play via the Android Publisher API, the\nPlay Developer Reporting API and the private Cloud Storage reports bucket; the\nApp Store via App Store Connect. 34 tools total, named consistently.\n\n**Installs and earnings, which most tooling cannot reach.** Play installs,\nratings and earnings have no REST API. They exist only as UTF-16 CSVs in a\nprivate Cloud Storage bucket, so most tooling skips them and \"how much did this\napp earn?\" simply cannot be answered. StorePilot reads that bucket, resolves CSV\ncolumns by name rather than position, and reports earnings per currency without\never summing across currencies. (This path is implemented and fixture-tested but\nhas not yet read a real report — see [Status](#status-and-honest-limits).)\n\n**Portfolio-first.** `portfolio_overview` renders every app on both stores in\none table. `play_portfolio_health` scans an entire Play account. Neither takes a\npackage name. Built for indie devs, app factories and agencies.\n\n**Cross-store tools.** `parity_check` finds listing and version drift between\nthe two stores. `compare_reviews` puts both stores' reviews side by side.\n`release_both` ships one version to both. `metadata_pull` / `metadata_push` use\nfastlane's own directory layout, so adopting StorePilot never means abandoning\nfastlane.\n\n**Writes are gated.** Every write previews first and returns a confirmation\ntoken that is HMAC-keyed to the exact operation, single-use, and expires in 10\nminutes. Production releases are forced into a staged rollout capped at 20%.\n\n**`setup_doctor`.** Credential setup here is a multi-step Google Cloud + Play\nConsole + App Store Connect maze with several silent failure modes. One tool\nchecks every step and prints the exact fix.\n\n---\n\n## Install\n\nRequires Python 3.11+.\n\n```bash\nuvx storepilot          # no install; uv fetches it per run\n```\n\nor install it:\n\n```bash\npipx install storepilot   # or: pip install storepilot\n```\n\nEither way you get a `storepilot` binary. Every client config below wants its\nabsolute path — `which storepilot` prints it.\n\n### Claude Code\n\n```bash\nclaude mcp add storepilot -- storepilot\n```\n\n### Claude Desktop / any MCP client\n\n`claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"storepilot\": {\n      \"command\": \"/absolute/path/to/storepilot\",\n      \"env\": {\n        \"STOREPILOT_GOOGLE_CREDENTIALS\": \"/path/to/service-account.json\",\n        \"STOREPILOT_GOOGLE_REPORTS_BUCKET\": \"pubsite_prod_rev_0123456789\",\n        \"STOREPILOT_ASC_KEY_PATH\": \"/path/to/AuthKey_XXXXXXXXXX.p8\",\n        \"STOREPILOT_ASC_KEY_ID\": \"XXXXXXXXXX\",\n        \"STOREPILOT_ASC_ISSUER_ID\": \"00000000-0000-0000-0000-000000000000\",\n        \"STOREPILOT_ASC_VENDOR_NUMBER\": \"80000000\"\n      }\n    }\n  }\n}\n```\n\n### Cursor\n\nSettings → MCP → Add new MCP server, or add to `~/.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"storepilot\": {\n      \"command\": \"/absolute/path/to/storepilot\",\n      \"env\": { \"STOREPILOT_ASC_KEY_PATH\": \"/path/to/AuthKey_XXXXXXXXXX.p8\" }\n    }\n  }\n}\n```\n\n### Codex CLI\n\nAdd to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.storepilot]\ncommand = \"/absolute/path/to/storepilot\"\nenv = { STOREPILOT_ASC_KEY_PATH = \"/path/to/AuthKey_XXXXXXXXXX.p8\" }\n```\n\nConfigure only the store you use. Each adapter registers its tools independently,\nand the cross-store tools register as soon as either store is configured. See\n[`.env.example`](.env.example) for every variable the code reads.\n\nA `.env` file also works, but it is read from the process working directory —\nwhich is whatever directory your MCP client happened to launch the server from,\nnot the repo. Use it when you run `storepilot` yourself from the checkout; use\nthe client's `env` block otherwise.\n\nCredentials are read from the environment and never written anywhere by\nStorePilot. Keep the service account JSON and the `.p8` outside the repo, and\n`chmod 600` both — they are private keys.\n\n---\n\n## Start with setup_doctor\n\nDo not start by calling a data tool and guessing why it returned nothing. Ask\nyour assistant to run `setup_doctor` first. It checks each credential step\nindependently, so one failure never hides the rest, and every failure carries\nits own fix.\n\nWith nothing configured yet:\n\n```\nStorePilot setup check\n============================================================\n\n-- Google Play ----------------------------------------------\n[skip] Google Play: not configured\n      Fix: Set STOREPILOT_GOOGLE_CREDENTIALS to the path of a service account JSON key. Full\n      setup: create a Google Cloud project, enable the Android Publisher and Play Developer\n      Reporting APIs, create a service account, download its JSON key, then invite the service\n      account email in Play Console -> Users and permissions.\n      Docs: https://developers.google.com/android-publisher/getting_started\n\n-- App Store Connect ----------------------------------------\n[fail] ASC credentials: App Store Connect is not configured: STOREPILOT_ASC_KEY_PATH,\n      STOREPILOT_ASC_KEY_ID, STOREPILOT_ASC_ISSUER_ID unset.\n      Fix: In App Store Connect go to Users and Access -> Integrations -> App Store Connect\n      API, create a team key with the Admin or App Manager role, and download the .p8 file\n      (Apple lets you download it exactly once). Then set STOREPILOT_ASC_KEY_PATH to that\n      file, STOREPILOT_ASC_KEY_ID to the Key ID column, and STOREPILOT_ASC_ISSUER_ID to the\n      Issuer ID shown above the key table. Sales reports additionally need\n      STOREPILOT_ASC_VENDOR_NUMBER from Payments and Financial Reports.\n      Docs: https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api\n[skip] ASC token: cannot test until the credentials load\n      Fix: Fix the step above, then re-run setup_doctor.\n[skip] ASC API reachable: cannot test until the credentials load\n      Fix: Fix the step above, then re-run setup_doctor.\n[skip] ASC sales access: cannot test until the credentials load\n      Fix: Fix the step above, then re-run setup_doctor.\n\n============================================================\nSummary: 1 fail, 4 skip\nResolve the [fail] items above, then run setup_doctor again.\n```\n\nIt also catches the failure that looks like success: without the **Reply to\nreviews** permission, Play's reviews API returns HTTP 200 and an empty list\nrather than an error, so \"this app has no reviews\" and \"you lack a permission\"\nare the same response. `setup_doctor` reports that ambiguity as its own\ndiagnosis instead of guessing.\n\nFull walkthrough for both stores: **[docs/SETUP.md](docs/SETUP.md)**.\n\n---\n\n## Tools\n\n34 tools. Full arguments, return shapes and per-tool limitations in\n**[docs/TOOLS.md](docs/TOOLS.md)**.\n\n### Google Play — read\n\n| Tool | What it answers |\n|---|---|\n| `play_list_apps` | Which packages can this install reach? |\n| `play_get_vitals` | Crash and ANR (Application Not Responding) rates vs Google's 1.09% / 0.47% thresholds |\n| `play_get_anomalies` | What did Google's own anomaly detection flag? |\n| `play_get_stats` | Installs and ratings for a month (GCS bucket) |\n| `play_get_earnings` | Earnings for a month, per currency (GCS bucket) |\n| `play_list_reviews` | Recent production reviews, filterable by star rating |\n| `play_portfolio_health` | Vitals + rating + installs for every app, one table |\n\n### Google Play — write (all gated)\n\n| Tool | What it does |\n|---|---|\n| `play_upload_bundle` | Upload an .aab and release it on a track |\n| `play_create_release` | Release already-uploaded version codes |\n| `play_promote_release` | Move a build between tracks (beta → production) |\n| `play_expand_rollout` | Widen a staged rollout — the only path to 100% |\n| `play_halt_rollout` | Stop a rollout **immediately**, no confirmation step |\n| `play_reply_review` | Publish a public developer reply |\n| `play_update_listing` | Overwrite listing copy for one locale |\n\n### App Store Connect\n\n| Tool | What it does |\n|---|---|\n| `asc_list_apps` | Apps visible to the key, with Apple IDs |\n| `asc_list_builds` | TestFlight builds and their three review states |\n| `asc_list_reviews` | Customer reviews, filterable, full history |\n| `asc_list_versions` | Versions, review state, phased-release day |\n| `asc_get_sales` | Sales and proceeds, cached hard |\n| `asc_get_analytics` | App Analytics via Apple's async reports chain |\n| `asc_upload_build` | Returns Transporter instructions — no API can upload |\n| `asc_reply_review` | Public developer response (gated) |\n| `asc_update_metadata` | Listing copy on the editable version (gated) |\n| `asc_submit_for_review` | Submit to App Review after a precheck (gated) |\n\n### Cross-store\n\n| Tool | What it does |\n|---|---|\n| `portfolio_overview` | Every app on both stores in one table |\n| `compare_reviews` | Both stores' reviews for one app, side by side |\n| `parity_check` | Version and listing drift between the stores |\n| `release_both` | One version to Play and TestFlight, one token (gated) |\n| `metadata_pull` | Store copy → fastlane-layout local tree |\n| `metadata_push` | Local tree → both stores (gated) |\n| `list_app_pairs` | Show the pairing registry |\n| `suggest_app_pairs` | Propose pairings from bundle-id and name evidence |\n| `pair_apps` | Write one pairing into the registry |\n\nPlus `setup_doctor`.\n\nNothing in either API says a Play package and an Apple ID are the same product,\nso the pairing lives in `~/.storepilot/apps.toml`. Run `suggest_app_pairs`, then\n`pair_apps` for the proposals you agree with. A proposal is inert until written —\na wrong pair would attribute one app's revenue, reviews and crash rate to\nanother, and nothing downstream would look wrong.\n\n---\n\n## The safety model\n\nEvery write is two calls. The first returns a preview and a token; the second\ncarries that token back.\n\nThe preview is the actual safety mechanism: it renders into the chat, where a\nhuman can read \"that is the wrong app\" or \"that is the production track\". Play\npreviews are not estimates — the operation runs for real inside a throwaway Play\nedit that is validated and then deleted, so the preview reports the version code\nGoogle actually assigned and any error Google would actually raise.\n\nThe token exists only to prevent drift between the preview and the confirmation.\nIt is an HMAC over a canonical fingerprint of the operation, keyed with a\nper-install secret kept out of tool output, single-use, and valid for 10 minutes.\nA plain content hash would be computable by the model itself, letting it\nself-confirm without ever rendering the preview a human needs to see. Change any\nargument and the token stops working.\n\nThree more rules:\n\n- **Production is forced into a staged rollout**, capped at 20% on the first\n  step (0.1 if you omit a fraction). `play_expand_rollout` is the only path to\n  100%, separated so it can never happen as a side effect.\n- **`play_halt_rollout` is deliberately ungated.** Stopping a bad release is\n  always the safe direction, and making someone do a second round trip during an\n  incident is a design failure.\n- **Everything is audited** — previewed, confirmed, rejected, executed, failed —\n  to an append-only log at `~/.storepilot/audit.log`, with credentials and long\n  values redacted.\n\n`release_both` issues **one** token for both stores, because approving half a\ntwo-store release is not approving the release. On partial failure it says\nexactly what landed where and rolls nothing back: pulling a working build from\nusers because the other store returned an API error is worse than the drift.\n\n---\n\n## Status and honest limits\n\nStorePilot has been run against real accounts on both stores, read-only. That\nfirst run found three bugs, all fixed: Apple rejects `sort` on the\nappStoreVersions endpoint, the Play reports bucket is not always named\n`pubsite_prod_rev_*`, and bucket access is an account-level Play Console\npermission rather than the Cloud Console IAM grant the error message used to\nrecommend.\n\n**Verified live** — service-account and ES256 JWT auth, rate-limit pacing,\n`setup_doctor` on both stores, the Play and App Store read tools, and\n`portfolio_overview` rendering six apps across both stores in one table.\n\n**Not yet verified, and two of them carry headline claims:**\n\n- **No vitals datapoint has ever come back.** Android Vitals suppresses metrics\n  below a minimum daily user count, and the test account's apps are under it. So\n  the threshold comparison — the thing `play_get_vitals` exists for — has never\n  run on real numbers, and the open question stands: does the Reporting API\n  return `1.09` or `0.0109`? A mismatch makes every verdict wrong by 100x.\n- **No report CSV has ever been read.** Installs, ratings and earnings all come\n  from the GCS bucket, and the parser has only ever seen fixtures. Google changed\n  the earnings report's Fee Description column in [July 2026][play-reports], so\n  the real headers are the risk.\n\n[play-reports]: https://support.google.com/googleplay/android-developer/answer/6135870\n- **No write has ever executed.** Every guard, preview and rollout policy is\n  proven against a fake client and 503 tests, never against a real store.\n- Apple sales reports return 403 for an App Manager key; reading them needs a\n  key with finance access.\n\nThe full list is in [docs/ROADMAP.md](docs/ROADMAP.md). If you run StorePilot\nagainst a real account — especially one with an app large enough to report\nvitals — those items are the most valuable bug reports you can file.\n\n### Hard limits, imposed by the stores\n\nThese cannot be fixed here, and StorePilot says so at the point of use rather\nthan returning something that looks like an answer:\n\n- **Creating a new app record is Console-only on both stores.** On Play the\n  first upload must also be manual.\n- **Declaration forms are Console-only** — Data safety, privacy nutrition\n  labels, content rating.\n- **No REST endpoint uploads an iOS binary.** `asc_upload_build` returns\n  Transporter/altool instructions instead of pretending.\n- **Play's reviews API** returns production-track reviews only, comment-bearing\n  reviews only, roughly the last 7 days only.\n- **Apple publishes no aggregate rating** through its API at all.\n- **Play report data lags 3–7 days**, and a month's earnings land around the 5th\n  of the following month. Before then the report does not exist, and StorePilot\n  reports \"not published\" rather than 0.\n\n### Quotas\n\n- Play Developer Reporting: 10 queries/second. StorePilot paces itself at 8 to\n  leave headroom, so a large portfolio scan takes a few seconds.\n- App Store Connect: ~3,600 requests/hour per key, reported in the\n  `x-rate-limit` header, plus an undocumented per-minute ceiling that starts\n  refusing around 300–350. The client paces against both, holding itself to 240\n  requests a minute and spacing calls out as the hourly budget runs down.\n- Apple's `salesReports` is far scarcer than the rest of the API, so results are\n  cached — past periods forever, since Apple never rewrites them.\n\n---\n\n## How it compares\n\nStorePilot is not the only MCP server touching these APIs, and it is not a\nfastlane replacement. [docs/COMPARISON.md](docs/COMPARISON.md) names the\nalternatives, says what each does better, and is specific about the narrow band\nwhere StorePilot is the only option.\n\n## Documentation\n\n- [docs/SETUP.md](docs/SETUP.md) — the credential maze, end to end\n- [docs/TOOLS.md](docs/TOOLS.md) — every tool, argument and limitation\n- [docs/COMPARISON.md](docs/COMPARISON.md) — alternatives, and fastlane\n- [docs/ROADMAP.md](docs/ROADMAP.md) — state and the live-verification backlog\n- [docs/SECURITY.md](docs/SECURITY.md) — credential handling and the threat model\n- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, ground rules, and what actually helps\n- [CHANGELOG.md](CHANGELOG.md) — what shipped, and what the live runs fixed\n\n## Contributing\n\nThe most useful contribution is running StorePilot against a real store account\nand reporting what breaks — particularly an account with an app large enough that\nAndroid Vitals reports data, which the test account was not. See\n[CONTRIBUTING.md](CONTRIBUTING.md).\n\n## MCP Registry\n\nPublished as `io.github.sonlenef/storepilot-mcp`.\n\nmcp-name: io.github.sonlenef/storepilot-mcp\n\n## License\n\nMIT\n",
  "bytes": 17684,
  "sha": "99a1f20efe7e2a3e32e77d55a2d0d90ca7605d5d8e77087c9fcb6f4a72f62e8d",
  "repo_slug": "sonlenef/storepilot-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_sonlenef_storepilot_mcp_037a6fe3/readme"
}