appstore-mcp
by forgeopslabs
README.md
# appstore-mcp
An [MCP](https://modelcontextprotocol.io) server, written in Rust, that exposes
the **Apple App Store Connect API** to AI agents. It covers the full product
lifecycle β apps & metadata, in-app purchases, subscriptions and their offers,
pricing & availability, App Store versions, App Review submission, TestFlight,
provisioning & signing, asset uploads, promoted purchases, customer reviews,
phased release, users & access, in-app events, Xcode Cloud, and analytics
reports β across **114 tools**, and can reach *any* other App Store Connect
endpoint through two generic JSON:API tools.
Built on the official [`rmcp`](https://crates.io/crates/rmcp) SDK over stdio.
π **[Full tool reference β docs/TOOLS.md](docs/TOOLS.md)** β every tool's purpose
and parameters.
Every tool is labelled with MCP annotations, so a client can tell `list_apps`
from `remove_user`. You can serve only the domains you need (`ASC_TOOLS`) or only
the tools that cannot write (`ASC_READ_ONLY`) β see
[Choosing which tools to serve](#choosing-which-tools-to-serve).
## Design: hybrid coverage
The App Store Connect API has hundreds of endpoints but is uniformly
[JSON:API](https://jsonapi.org). Rather than a tool per endpoint, this server is
**hybrid**:
- **Curated tools** (112) for the common, multi-step, or error-prone workflows β
apps & metadata, IAPs, subscriptions & offers, versions, pricing, availability,
App Review submission, TestFlight, provisioning, asset uploads, promoted
purchases, customer reviews, phased release, users, in-app events, Xcode Cloud,
analytics reports, and custom product pages.
- **Two generic escape-hatch tools** β `appstore_request` and `appstore_list` β
that can call *any* endpoint with raw JSON:API documents.
## Tools
| Group | Tools |
|------|-------|
| **Generic** | `appstore_request`, `appstore_list` |
| **Apps & metadata** | `list_apps`, `get_app`, `update_app`, `list_app_infos`, `update_app_info`, `set_age_rating`, `create_app_info_localization`, `update_app_info_localization` |
| **In-app purchases (v2)** | `list_in_app_purchases`, `create_in_app_purchase`, `update_in_app_purchase`, `delete_in_app_purchase`, `create_iap_localization`, `set_iap_price_schedule`, `upload_iap_review_screenshot` |
| **Subscriptions** | `list_subscription_groups`, `create_subscription_group`, `create_subscription`, `update_subscription`, `create_subscription_localization`, `set_subscription_price` |
| **Versions & metadata** | `list_app_store_versions`, `create_app_store_version`, `create_version_localization`, `update_version_localization` |
| **App Review submission** | `create_review_submission`, `add_review_submission_item`, `submit_review_submission`, `list_review_submissions`, `submit_in_app_purchase`, `set_app_review_detail`, `create_app_encryption_declaration`, `assign_build_encryption_declaration` |
| **Pricing** | `list_territories`, `list_iap_price_points`, `list_subscription_price_points` |
| **Availability** | `set_iap_availability`, `set_subscription_availability`, `set_app_availability` |
| **TestFlight** | `list_builds`, `list_beta_groups`, `create_beta_group`, `add_beta_tester`, `submit_build_for_beta_review`, `set_build_test_notes`, `set_build_beta_detail`, `set_beta_app_review_detail`, `expire_build`, `add_build_to_beta_group` |
| **Provisioning & signing** | `list_bundle_ids`, `create_bundle_id`, `enable_bundle_id_capability`, `disable_bundle_id_capability`, `list_certificates`, `create_certificate`, `list_devices`, `register_device`, `list_profiles`, `create_profile` |
| **Assets** | `upload_app_screenshot`, `upload_app_preview`, `create_screenshot_set`, `create_preview_set`, `delete_screenshot_set`, `delete_preview_set`, `reorder_screenshots` |
| **Subscription offers** | `create_introductory_offer`, `create_promotional_offer`, `create_winback_offer`, `list_winback_offers` |
| **Offer codes** | `create_offer_code`, `generate_one_time_use_codes`, `create_custom_offer_code`, `list_offer_codes` |
| **Promoted purchases** | `create_promoted_purchase`, `update_promoted_purchase`, `set_promoted_purchase_order`, `list_promoted_purchases` |
| **Customer reviews** | `list_customer_reviews`, `respond_to_review`, `delete_review_response` |
| **Phased release** | `start_phased_release`, `update_phased_release` |
| **Users & access** | `list_users`, `invite_user`, `update_user`, `remove_user` |
| **In-app events** | `create_app_event`, `create_app_event_localization`, `upload_app_event_screenshot` |
| **Xcode Cloud** | `list_ci_products`, `list_ci_workflows`, `start_ci_build`, `get_ci_build_run`, `list_ci_build_actions` |
| **Analytics reports** | `request_analytics_report`, `list_analytics_reports`, `list_analytics_report_instances`, `list_analytics_report_segments`, `download_analytics_segment` |
| **Custom product pages** | `list_custom_product_pages`, `get_custom_product_page`, `create_custom_product_page`, `update_custom_product_page`, `delete_custom_product_page`, `list_custom_product_page_versions`, `create_custom_product_page_version`, `list_custom_product_page_localizations`, `create_custom_product_page_localization`, `update_custom_product_page_localization`, `create_cpp_screenshot_set`, `create_cpp_preview_set` |
See **[docs/TOOLS.md](docs/TOOLS.md)** for each tool's description and parameters. Custom product page
images are uploaded with the existing `upload_app_screenshot` / `upload_app_preview` tools.
## Install
Prebuilt binaries for **macOS (universal), Linux (x86-64), and Windows (x86-64)**
are attached to every [GitHub Release](https://github.com/forgeopslabs/appstore-mcp/releases).
Pick the channel for your client; all of them need credentials (see [Credentials](#credentials)).
### Claude Desktop β one-click bundle
Download `appstore-mcp.mcpb` from the latest release and open it with Claude Desktop
(**Settings β Extensions β Install Extensionβ¦**, or drag the file onto the window).
It prompts for your **Issuer ID**, **Key ID**, and **.p8 key file**. The bundle ships
all three platforms' binaries and selects the right one automatically.
### Claude Code β plugin marketplace
```text
/plugin marketplace add forgeopslabs/appstore-mcp
/plugin install appstore-mcp@forgeopslabs
```
The plugin launches the `appstore-mcp` binary from your `PATH`, so install it first β
download the binary for your OS from the [latest release](https://github.com/forgeopslabs/appstore-mcp/releases/latest)
and put it on your `PATH`, or `cargo install --git https://github.com/forgeopslabs/appstore-mcp`.
Set `ASC_ISSUER_ID`, `ASC_KEY_ID`, and `ASC_PRIVATE_KEY_PATH` in the environment you start
Claude Code from.
### Codex
Codex configures MCP servers directly (no marketplace). With `appstore-mcp` on your `PATH`:
```bash
codex mcp add appstore \
--env ASC_ISSUER_ID=... --env ASC_KEY_ID=... \
--env ASC_PRIVATE_KEY_PATH=/path/AuthKey_XXXXXX.p8 \
-- appstore-mcp
```
or in `~/.codex/config.toml`:
```toml
[mcp_servers.appstore]
command = "appstore-mcp"
args = []
env = { ASC_ISSUER_ID = "...", ASC_KEY_ID = "...", ASC_PRIVATE_KEY_PATH = "/path/AuthKey_XXXXXX.p8" }
```
### MCP Registry
Published as `io.github.forgeopslabs/appstore-mcp` (metadata in
[`server.json`](server.json)) so any MCP-aware client can discover it.
### From source
```bash
cargo build --release # -> target/release/appstore-mcp
```
## Credentials
Generate a **Team Key** in App Store Connect β *Users and Access β Integrations β
App Store Connect API*, and download the `.p8` file. Then set:
| Variable | Required | Description |
|----------|----------|-------------|
| `ASC_ISSUER_ID` | β
| Issuer UUID shown above the keys table. |
| `ASC_KEY_ID` | β
| The API key's Key ID. |
| `ASC_PRIVATE_KEY` | one of | Inline `.p8` PEM contents. |
| `ASC_PRIVATE_KEY_PATH` | one of | Path to the downloaded `.p8` file. |
| `ASC_BASE_URL` | optional | Override the API origin. |
| `ASC_LOG` | optional | Log filter (to stderr). Default `info`. |
See [`.env.example`](.env.example). The server authenticates each request with a
short-lived **ES256 JWT** signed by your key (cached and refreshed automatically).
> The server starts even without credentials so a client can list its tools;
> tool calls then return an actionable configuration error until creds are set.
## Choosing which tools to serve
A hundred tool definitions cost context in every session, and a client that sees
one flat list can't tell a read from a delete. Two knobs fix that:
| Variable | Default | Description |
|----------|---------|-------------|
| `ASC_TOOLS` | all | Comma-separated tool groups to serve, or the preset `core`. |
| `ASC_READ_ONLY` | `0` | Serve only tools that cannot modify the account. |
```bash
ASC_TOOLS=core # 41 tools: generic, apps, versions, assets, testflight, submission
ASC_TOOLS=testflight,provisioning # just what a build-distribution agent needs
ASC_READ_ONLY=1 # 35 read-only tools; writes are withheld entirely
```
Groups: `generic`, `apps`, `iap`, `subscriptions`, `versions`, `pricing`,
`availability`, `submission`, `testflight`, `provisioning`, `assets`, `offers`,
`offer-codes`, `promotions`, `reviews`, `users`, `events`, `xcode-cloud`,
`analytics`, `custom-product-pages` β plus `all` and `core`. An unrecognised name
is warned about on stderr and serves nothing rather than quietly falling back to
everything.
In read-only mode `appstore_request` is kept but refuses any method other than
`GET`, so the escape hatch still reaches endpoints without a curated tool without
becoming a way around the restriction.
Every served tool advertises MCP annotations (`readOnlyHint`, `destructiveHint`,
`idempotentHint`), which clients use to decide what needs confirming. Nine tools
are marked destructive: the seven `delete_*`/`remove_*` tools, `expire_build`,
`disable_bundle_id_capability`, and `appstore_request` (which can reach any
`DELETE` endpoint).
## Tuning
Defaults are chosen so a tool call can't hang and a single response can't swamp
an agent's context. All of these are optional.
| Variable | Default | Description |
|----------|---------|-------------|
| `ASC_TIMEOUT_SECS` | `60` | Whole-request timeout. `0` disables. |
| `ASC_CONNECT_TIMEOUT_SECS` | `10` | Connect timeout. `0` disables. |
| `ASC_TRANSFER_TIMEOUT_SECS` | `300` | Timeout for asset uploads and report downloads. |
| `ASC_MAX_RETRIES` | `3` | Retries after the first attempt. `0` disables. |
| `ASC_MAX_RESPONSE_BYTES` | `60000` | Tool-result size cap. `0` disables. |
| `ASC_COMPACT_RESPONSES` | `1` | Strip redundant JSON:API links from responses. |
**Retries.** A `429` is replayed for any method, since Apple rejected the request
without applying it. A `5xx` or a mid-flight timeout is replayed only for
`GET`/`PATCH`/`PUT`/`DELETE` β never `POST`, which could otherwise create a
duplicate resource (and Apple permanently reserves identifiers like a product
ID). Backoff is exponential with jitter and honours `Retry-After`.
**Response shaping.** Results are serialized compactly β indented JSON measured
1.72Γ the bytes for identical content, so the same budget now carries about 40%
more of the data you asked for. Per-resource `self` links and link-only
relationships are stripped: no addressable content is lost, and `links.next`
survives for pagination. If a response still exceeds the budget, `included` is
dropped first, then trailing `data` items, and the result carries a `_truncated`
key saying what went missing and how to narrow the query. Note that following
`links.next` after a trim would skip the dropped items β re-request with a
smaller `limit` instead.
## Build & run
```bash
cargo build --release
ASC_ISSUER_ID=... ASC_KEY_ID=... ASC_PRIVATE_KEY_PATH=/path/AuthKey_XXX.p8 \
./target/release/appstore-mcp
```
The server speaks MCP over **stdio**. Logs go to **stderr**; stdout is the
protocol channel.
### Use with an MCP client
Example client config (e.g. Claude Desktop's `mcpServers`):
```json
{
"mcpServers": {
"appstore": {
"command": "/absolute/path/to/appstore-mcp/target/release/appstore-mcp",
"env": {
"ASC_ISSUER_ID": "00000000-0000-0000-0000-000000000000",
"ASC_KEY_ID": "ABCD123456",
"ASC_PRIVATE_KEY_PATH": "/absolute/path/to/AuthKey_ABCD123456.p8"
}
}
}
}
```
### Inspect with the MCP Inspector
```bash
npx @modelcontextprotocol/inspector ./target/release/appstore-mcp
```
## Usage notes
- **IDs are opaque.** List/get first to resolve app, IAP, subscription, set, and
price-point IDs, then pass them to create/update tools.
- **Pricing needs a price point.** Use `list_iap_price_points` /
`list_subscription_price_points` to get the `id` for `set_iap_price_schedule` /
`set_subscription_price`.
- **Asset uploads** (`upload_*`) take a local file path and run the full reserve β
chunked upload β MD5 commit flow in one call. The file is streamed, so peak
memory is one chunk rather than the size of the asset, and a chunk that fails
is retried on its own. Screenshots/previews require an existing
`appScreenshotSet` / `appPreviewSet`; create those with the generic tools if
needed.
- **Pagination.** `appstore_list` returns one page by default. Pass
`max_pages` (up to 20) to follow `links.next` and merge the pages into one
result β `meta.hasMore` tells you whether anything is left.
- **Analytics data.** `request_analytics_report` β `list_analytics_reports` β
`list_analytics_report_instances` β `list_analytics_report_segments` gets you a
presigned segment URL; `download_analytics_segment` fetches it, gunzips it, and
returns the rows as JSON. Apple can take up to 48 hours to generate the first
report for a new request.
- **Anything not listed** is reachable via `appstore_request` (raw method + path +
JSON:API body) or `appstore_list` (paginated GET). Example:
`appstore_request { "method": "GET", "path": "/v1/apps/123/customerReviews" }`.
- **Not covered:** sales/finance report endpoints return gzipped TSV (not JSON:API)
and are out of scope for these tools.
## Limitations (enforced by Apple)
- **You cannot create an app via the API.** The `apps` resource only allows GET and
UPDATE β `POST /v1/apps` returns `403 FORBIDDEN_ERROR`. Create the app record in
the [App Store Connect website](https://appstoreconnect.apple.com) (*Apps β β β
New App*); you can pre-create its bundle ID with `create_bundle_id`. All other
tools operate on an existing app.
- A deleted in-app purchase's `productId` is permanently reserved by Apple and
cannot be reused.
## Development
```bash
cargo test # 200+ tests, no network or credentials needed
cargo clippy --all-targets -- -D warnings
cargo fmt --check
```
Tests come in three layers: pure unit tests for request-body builders, retry
decisions, and response shaping; [`wiremock`](https://crates.io/crates/wiremock)
tests that drive the real HTTP client against a mock API (retries, timeouts,
pagination, the three-step upload protocol, segment downloads); and
`tests/tool_surface.rs`, which asserts the invariants of what a client actually
sees β unique names, real descriptions, object schemas, correct annotations, and
that `ASC_TOOLS`/`ASC_READ_ONLY` withhold exactly what they claim to.
The minimum supported Rust version is **1.88**, checked by its own CI job.
Regenerate the tool reference after adding/changing tools (needs the release
binary; no credentials required):
```bash
cargo build --release && python3 scripts/gen_tools_doc.py # rewrites docs/TOOLS.md
```
### Live integration tests
`scripts/integration_test.py` drives the compiled server against the real API.
Read-only by default; `--write` adds a self-cleaning IAP lifecycle.
```bash
cargo build --release
# Credentials via env (ASC_ISSUER_ID/ASC_KEY_ID/ASC_PRIVATE_KEY_PATH) or local
# appstore-connect.txt + AuthKey_*.p8 in the repo root (both gitignored).
python3 scripts/integration_test.py --app <APP_ID> # read-only sweep
python3 scripts/integration_test.py --app <APP_ID> --write # + write lifecycle
```
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessUnresponsive