npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mgcrea/mcp-appstore-connect

v0.10.0

Published

Model Context Protocol server for the Apple App Store Connect API

Readme

@mgcrea/mcp-appstore-connect

npm version GHCR

Model Context Protocol server for the Apple App Store Connect API — inspect your apps, versions, builds, TestFlight, sales and users, and (opt-in) edit metadata and manage testers, straight from an MCP client like Claude.

Unofficial. Not affiliated with or endorsed by Apple. It talks to Apple's public App Store Connect REST API using an API key you generate yourself.

Features

  • Broad coverage — apps, App Store versions & localizations, builds, TestFlight groups/testers/feedback, sales & finance reports, analytics, users, bundle ids & capabilities, devices.
  • Listing round-trip — export the whole store listing to a git-committable metadata tree, edit it locally, apply it back with digest-based conflict detection.
  • Read by default, writes opt-in — mutating tools are not registered at all unless you ask for them. See Security.
  • Typed & tested — ESM, built with tsdown, linted/formatted with oxc, tested with vitest. Tests run fully offline.

Security

You are pointing an AI agent at the account that ships your apps, so the honest details matter more than reassurance.

Supply chain

Two direct dependencies: @modelcontextprotocol/sdk and zod. Nothing else is chosen by us.

Being straight about what that actually costs: those two pull in ~94 packages transitively — the number npm install prints, and every one of them arrives via the official MCP SDK. That's the honest figure, not "two dependencies". Two things keep the real exposure much smaller than 94:

  • Nothing runs at install time. Not one package in the tree declares a preinstall, install or postinstall script, so npm install executes no third-party code — the most common supply-chain attack path simply isn't open.
  • Only 5 are reachable when the server runs: the SDK, zod, ajv, ajv-formats and zod-to-json-schema. This server speaks stdio only, so the SDK's HTTP/SSE/OAuth stack (express, hono, jose, cors, pkce-challenge, eventsource) sits in the tree but is never imported.

Check all of it yourself:

npm view @mgcrea/mcp-appstore-connect dependencies       # the two
npm ls --omit=dev --all                                  # the ~94
grep -hoE '^import[^;]*from "[^"]+"' node_modules/@mgcrea/mcp-appstore-connect/dist/*.js

That last command prints everything the shipped bundle imports — the SDK's stdio entrypoints, zod, and Node builtins. Nothing else.

Verified builds

Neither artifact is published from a laptop:

Both trace back to the exact commit and CI run that produced them. The commands to check are in Verify — please run them rather than take this section's word for it.

Your credentials

The .p8 never leaves your machine, and never goes over the wire. Tokens are minted locally: the server signs short-lived ES256 JWTs (20-minute cap, re-signed just before expiry) using Node's built-in node:crypto. There is no jsonwebtoken or jose in the signing path — one less dependency between your private key and the network. Under Docker the key is mounted read-only and is never baked into the image.

The server never writes to your disk. export_listing hands back {path, content} pairs and your agent writes them, so every file write stays under your own MCP client's permission prompt rather than happening invisibly inside the server.

Blast radius

Three independent limits, smallest first:

  1. Writes are off by default. Mutating tools aren't merely refused when APP_STORE_CONNECT_ALLOW_WRITES=1 is unset — they are never registered, so they don't appear in the tool list and a confused agent cannot call them. The default install is read-only.
  2. Destructive tools need confirm: true. Deleting a screenshot, removing a tester, or submitting a version to Apple takes an explicit acknowledgement argument, so it can't happen as a side effect of some broader request. Submitting is the one that leaves your account — it is gated the same way, and refuses outright unless the version is in a submittable state with a build attached.
  3. Your API key's role is the real ceiling, and this server can't raise it. A read-only role is enough for every list/get tool; issue one of those and no bug here can write anything. Scope the key to the narrowest role that does your job.

Applying a listing has its own rails — digest-based conflict detection, an allowClear gate before any field is emptied, and a whole-apply abort if any field is over Apple's limit. See Listing round-trip.

Configure

Create a key in App Store Connect → Users and Access → Integrations → Keys → App Store Connect API. Apple gives you an Issuer ID, a Key ID, and a one-time .p8 download. Then set:

| Variable | Required | Notes | | --------------------------------- | -------- | -------------------------------------------------------------- | | APP_STORE_CONNECT_KEY_ID | yes | The 10-char Key ID. | | APP_STORE_CONNECT_ISSUER_ID | yes | The Issuer ID (a UUID). | | APP_STORE_CONNECT_P8_PATH | one of | Path to the AuthKey_XXXX.p8 file. | | APP_STORE_CONNECT_P8 | one of | Inline PEM contents (for Docker/CI); set this or the path. | | APP_STORE_CONNECT_VENDOR_NUMBER | reports | Needed only by the sales/finance report tools. | | APP_STORE_CONNECT_ALLOW_WRITES | no | 1 to register the write tools. Off by default. | | APP_STORE_CONNECT_MAX_RETRIES | no | Retry budget for 401/429/5xx. Defaults to 3. | | APP_STORE_CONNECT_METADATA_ROOT | no | Where the listing tree lives. Defaults to fastlane/metadata. | | APP_STORE_CONNECT_DEBUG | no | 1 to log to stderr. |

See .env.example for the annotated list.

Config file

If you'd rather not put credentials in your shell profile or in every MCP client config, the server reads a config file instead:

// ~/.config/appstore-connect/config.json   (chmod 600)
{
  "keyId": "XXXXXXXXXX",
  "issuerId": "00000000-0000-0000-0000-000000000000",
  "p8Path": "~/path/to/AuthKey_XXXXXXXXXX.p8",
  "allowWrites": true,
}
mkdir -p ~/.config/appstore-connect
$EDITOR ~/.config/appstore-connect/config.json
chmod 600 ~/.config/appstore-connect/config.json

With this in place an MCP client needs no env block at all — just npx -y @mgcrea/mcp-appstore-connect.

  • The environment wins, field by field. A config file supplies whatever the environment doesn't, so Docker and CI keep working exactly as before, and a one-off APP_STORE_CONNECT_ALLOW_WRITES=0 still overrides a file that says true.
  • Keys are camelCase (keyId, not APP_STORE_CONNECT_KEY_ID), ~ is expanded in p8Path, and p8 takes an inline PEM as the alternative to p8Path.
  • metadataRoot sets where this machine's repos keep their listing tree — useful if you prefer "AppStore" to the fastlane default. It must be repo-relative; use "." for the repo root.
  • Unknown keys are an error, not ignored — a typo'd keyID tells you so instead of silently falling back to the environment.
  • Location: $APP_STORE_CONNECT_CONFIG, else $XDG_CONFIG_HOME/appstore-connect/config.json, else ~/.config/appstore-connect/config.json. An absent file is fine; a malformed one is reported with its path.
  • The server warns on stderr if the file is readable by other users.

The API key's role (set when you create it) decides what it can touch. A read-only role is enough for the list/get tools; editing metadata or managing testers needs App Manager or Admin. Team-scoped keys may require a JWT scope claim — if a call fails with 401 NOT_AUTHORIZED, that's the likely cause.

Quick start

Pick one of the three. All talk to the same App Store Connect API — the difference is only how the server is launched. Options A and B need nothing checked out.

A. npx — recommended

Zero install; npx fetches and runs the published package. Wire it into Claude Code (or any MCP client) with your credentials:

{
  "mcpServers": {
    "appstore-connect": {
      "command": "npx",
      "args": ["-y", "@mgcrea/mcp-appstore-connect"],
      "env": {
        "APP_STORE_CONNECT_KEY_ID": "XXXXXXXXXX",
        "APP_STORE_CONNECT_ISSUER_ID": "00000000-0000-0000-0000-000000000000",
        "APP_STORE_CONNECT_P8_PATH": "/absolute/path/to/AuthKey_XXXXXXXXXX.p8"
      }
    }
  }
}

To try it from a shell (reads the same env, or the config file):

npx -y @mgcrea/mcp-appstore-connect

B. Docker (stdio)

Runs the container image published to GHCR. The .p8 never goes into the image or the config — mount it read-only and point APP_STORE_CONNECT_P8_PATH at the in-container path. The -e VAR (no value) flags forward the key id / issuer id from the env block, so no secret sits in args:

{
  "mcpServers": {
    "appstore-connect": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "APP_STORE_CONNECT_KEY_ID",
        "-e",
        "APP_STORE_CONNECT_ISSUER_ID",
        "-e",
        "APP_STORE_CONNECT_P8_PATH=/keys/key.p8",
        "-v",
        "/absolute/path/to/AuthKey_XXXXXXXXXX.p8:/keys/key.p8:ro",
        "ghcr.io/mgcrea/mcp-appstore-connect:latest"
      ],
      "env": {
        "APP_STORE_CONNECT_KEY_ID": "XXXXXXXXXX",
        "APP_STORE_CONNECT_ISSUER_ID": "00000000-0000-0000-0000-000000000000"
      }
    }
  }
}

-i keeps stdin open, which the stdio transport needs — don't drop it. The left side of -v is the host path to your .p8; the container only ever sees /keys/key.p8. GHCR is the only registry CI publishes to — it's what carries the provenance/SBOM/cosign signature described in Verify below.

C. From source (development)

git clone https://github.com/mgcrea/mcp-appstore-connect.git
cd mcp-appstore-connect
pnpm install
pnpm build
node dist/cli.js        # credentials from the env or the config file

Or wire the built entry directly: "command": "node", "args": ["/absolute/path/to/mcp-appstore-connect/dist/cli.js"].

Inspect the tools

npx @modelcontextprotocol/inspector npx -y @mgcrea/mcp-appstore-connect

Tools

Appslist_apps, get_app

Listing round-tripexport_listing, apply_listing* — pull the whole listing into a git-committable metadata tree, edit it locally, push it back. See Listing round-trip.

Versions & metadatalist_versions, list_version_localizations, get_version_localization, create_version*, update_version* (release type — auto on approval, manual, or scheduled), update_version_localization* (description, keywords, what's-new, promo text)

Review submissionslist_review_submissions, submit_version_for_review*†, cancel_review_submission*† — hand a finished version to Apple for review, or withdraw it

Releaserelease_version*† — release an approved version sitting in PENDING_DEVELOPER_RELEASE (the manual "Release This Version" button)

App infolist_app_infos, list_app_info_localizations, get_app_info_localization, update_app_info_localization* (name, subtitle, privacy policy — the fields that outlive a version)

In-app purchaseslist_in_app_purchases, get_in_app_purchase, list_iap_price_points, get_iap_price_schedule, set_in_app_purchase_price*† — read the price-point catalogue for a territory, then price the IAP against one. One-time purchases only; auto-renewable subscriptions are not covered.

Screenshotslist_screenshot_sets, list_screenshots, get_screenshot, upload_screenshot*, delete_screenshot*†, delete_screenshot_set*†, reorder_screenshots*†

Buildslist_builds

TestFlightlist_beta_groups, list_beta_testers, list_beta_feedback, invite_beta_tester*, add_tester_to_group*, remove_tester_from_group*†

Reports & analyticsdownload_sales_report, download_finance_report, list_analytics_reports, create_analytics_report_request*

Userslist_users

Bundle IDslist_bundle_ids, get_bundle_id, create_bundle_id*, enable_capability*, disable_capability*†

Deviceslist_devices, register_device*

Italic* tools are writes, hidden unless APP_STORE_CONNECT_ALLOW_WRITES=1. † additionally requires confirm: true.

Tool names are prefixed app_store_connect_ (omitted above for brevity).

A Claude Code skill that drives these tools through a full release ships alongside the server — see Release-prep plugin.

Listing round-trip

export_listing returns the complete listing — name, subtitle, description, keywords, what's-new, promotional text and URLs, across every locale — as a set of files to write into your repo:

fastlane/metadata/
  .listing.json          # ids + baseline digests. Commit it; never hand-edit it.
  en-US/
    name.txt  subtitle.txt  description.txt  keywords.txt
    release_notes.txt  promotional_text.txt
    marketing_url.txt  support_url.txt  privacy_url.txt
  fr-FR/
    ...

This is the layout fastlane deliver already uses, so the tree interops with it. One file per field means the file content is the value, byte for byte — a description containing ## Keywords, a --- rule or a fenced code block is just text, and git diff shows you the field that changed rather than a line number in a wall of copy.

The location is a default, not a requirement. Keep the tree wherever you like — set APP_STORE_CONNECT_METADATA_ROOT (or metadataRoot in the config file) to change it for every repo on the machine, or pass metadataRoot to a single export_listing call. The fastlane path is the default only because it's the one other tools already read. apply_listing needs no such setting: it finds the tree from wherever you pass .listing.json, so a tree you move later keeps working.

The server never writes to disk: export_listing hands back {path, content} pairs and your agent writes them, so every write stays under your own permission prompt and nothing depends on host paths being visible inside Docker.

Editing and pushing back:

export_listing { appId }                       # version defaults to "latest"
# ...edit the .txt files, commit, review...
apply_listing  { files: [...] }                # dry run by default
apply_listing  { files: [...], dryRun: false, confirm: true }
  • version accepts "latest" (the one you're preparing), "live" (on sale) or an exact "1.4.0". Versions are ordered numerically, so 1.10.0 beats 1.9.0.
  • Pass only the files you changed, plus .listing.json — it carries the localization ids and the per-field digests recorded at export, and its directory is what tells the server where the tree lives. Every file you pass must sit under that same directory; mixing two trees is an error rather than a silent push against the wrong ids.
  • Those digests make apply a three-way merge. A field edited in App Store Connect's web UI since your export is reported as a conflict and skipped, rather than silently overwritten; re-export and merge, or pass force: true.
  • An absent file leaves a field alone; an empty file clears it — but clearing needs allowClear: true, so a file truncated by accident is reported as blocked rather than wiping live copy.
  • Any field over Apple's limit aborts the whole apply before the first write — a half-applied listing is worse than an untouched one.
  • format: "review" renders a read-only markdown summary with character counts, for when you just want to read the listing. Nothing parses it back.
  • If the metadata tree already exists, diff before overwriting it.

Release-prep plugin

This repo doubles as a Claude Code plugin marketplace. The appstore-toolkit plugin bundles the appstore-release-prep skill, which drives the round-trip above: it audits what shipped since the last release, writes the CHANGELOG entry and every store field within Apple's limits, and pushes the result back through apply_listing.

/plugin marketplace add mgcrea/mcp-appstore-connect
/plugin install appstore-toolkit@mgcrea-appstore

Installing it also wires up the appstore-connect MCP server, so the skill and the tools it calls arrive together. The plugin stores no credentials of its own — set up the config file once and the server finds them wherever you work.

apply_listing is a write tool, so it stays hidden until writes are enabled ("allowWrites": true in the config file, or APP_STORE_CONNECT_ALLOW_WRITES=1). That is deliberate: installing a plugin should not silently grant it permission to overwrite a live App Store listing.

The skill also ships an offline auditor (scripts/audit_release.py, stdlib Python, no network calls) that measures every field against its limit and exits non-zero when one is over or missing, so it can gate a release from CI.

Notes

  • Tokens are minted locally. Each request carries a fresh-enough ES256 JWT (aud: appstoreconnect-v1), cached and re-signed shortly before Apple's 20-minute cap. The .p8 never leaves your machine.
  • Reports are TSV, not JSON. download_sales_report / download_finance_report gunzip Apple's report and return the text (truncated to maxLines). Reports lag ~24–48h and are keyed by date/frequency.
  • Analytics is asynchronous. Create a report request, wait for Apple to generate it, then list its reports.
  • upload_screenshot reads the file server-side. Pass an absolute filePath the server can reach. Under Docker that means a path inside the container — mount the folder (-v /host/screenshots:/screenshots) and pass the container path, or send small images inline as base64 via fileData.
  • Screenshots validate after upload. Apple checks pixel dimensions asynchronously, so a wrongly-sized image fails during processing rather than at upload; the tool waits (waitSeconds, default 60) and reports Apple's exact reason. Timing out is not a failure — the upload already succeeded, so poll get_screenshot instead of retrying. The version must be editable (PREPARE_FOR_SUBMISSION or DEVELOPER_REJECTED), and a set holds at most 10 screenshots.
  • Screenshot order is explicit. reorder_screenshots replaces a set's full contents, so pass every id you want to keep — an omitted one is removed from the set.

Develop

pnpm dev            # tsdown --watch
pnpm test           # vitest (offline; no real credentials needed)
pnpm typecheck      # tsc --noEmit
pnpm lint           # oxlint
pnpm format         # oxfmt --write .

Tests run entirely offline: JWT signing is verified against a throwaway P-256 key, and the tools are driven over an in-memory MCP transport with a mocked fetch.

Publish

Options A (npx) and B (Docker) resolve only once a release is out. Pushing a v*.*.* tag triggers CI to:

  • publish to npm via Trusted Publishing (OIDC — no NPM_TOKEN stored anywhere) with a provenance attestation, and
  • build, sign, and push the multi-arch image to ghcr.io/mgcrea/mcp-appstore-connect, with build provenance, an SBOM, and a cosign keyless signature.

Both artifacts are cryptographically traceable back to the exact commit and CI run that produced them — see Verify below. Until a release exists, use Option C from source.

Verify

Before trusting an artifact from Option A or B, you can check it was actually built by this repo's CI rather than published from someone's laptop:

# npm — provenance attestation (also shown as a badge on the npmjs.com package page)
npm audit signatures

# Docker — cosign keyless signature, tied to this repo's GitHub Actions identity
cosign verify \
  --certificate-identity-regexp 'https://github.com/mgcrea/mcp-appstore-connect/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/mgcrea/mcp-appstore-connect:latest

License

MIT — Olivier Louvignes