Skip to main content
Glama

app-store-connect-mcp

An MCP server for both of Apple's commerce APIs — App Store Connect (1,263 operations) and the App Store Server API / StoreKit 2 (30 operations) — behind five tools, with the private key in the macOS Keychain and consequential writes gated behind an explicit confirmation.

1,293 operations · 5 tools · key never on disk · verified against the live APIs

Why it is built this way

There are several App Store Connect MCP servers. Each solves part of the problem; this one takes the part each got right and drops what they got wrong.

Approach

Kept

Rejected

Hand-wrapped tools

One MCP tool per endpoint

Typed, discoverable arguments

70–900 tool definitions, >100k tokens, stale the moment Apple ships a version

Code Mode

LLM writes JS, server evals it

Two tools, ~1k tokens, full coverage

Executes generated code in a process holding a signing key

Meta-tools

searchcall with parameters

Same context win, no code execution

This server uses the third. Coverage is a property of Apple's spec, not of how many endpoints someone wrapped; and the model never gets to run code inside a process that can change your pricing.

On the sandbox

Code Mode's premise is that generated JavaScript runs safely inside Node's vm. It does not. Node's own documentation says vm is not a security mechanism, and any host object injected as a global hands back the host realm through its own prototype chain:

spec.constructor.constructor('return process.env.HOME')()   // → /Users/you

Verified against a faithful reproduction of that sandbox: it returns the host environment. The timeout option does not help either — it only bounds synchronous execution, so an async busy-loop runs forever and starves the event loop.

Parameterised dispatch gets the same coverage and the same token cost with no interpreter to escape.

Related MCP server: App Store Connect MCP Server

Credentials

The private key belongs in the Keychain. Apple lets you download a .p8 exactly once, and a plaintext copy on disk is a copy that can leak.

ASC_KEY=keychain:my-asc-key          # recommended
ASC_KEY=/path/to/AuthKey.p8          # works, but plaintext
ASC_PRIVATE_KEY='-----BEGIN…'        # discouraged: `ps -E` exposes it

A Keychain item may hold a bare PEM, or base64 JSON:

{ "issuerID": "…", "keyID": "…", "privateKeyPEM": "-----BEGIN PRIVATE KEY-----\n…" }

The envelope form is worth preferring: the identifiers travel with the key material, so ASC_KEY_ID cannot drift out of sync with the key it names — a mismatch that surfaces only as an opaque 401.

security add-generic-password -s my-asc-key -a api -w "$(
  jq -nc --arg i "$ISSUER" --arg k "$KEYID" --arg p "$(cat AuthKey.p8)" \
    '{issuerID:$i,keyID:$k,privateKeyPEM:$p}' | base64
)"

Install

git clone https://github.com/abd3lraouf-studios/app-store-connect-mcp
cd app-store-connect-mcp
npm install && npm run build
{
  "mcpServers": {
    "app-store-connect": {
      "command": "node",
      "args": ["/path/to/app-store-connect-mcp/dist/index.js"],
      "env": {
        "ASC_KEY": "keychain:my-asc-key",
        "ASC_BUNDLE_ID": "com.example.app"
      }
    }
  }
}

ASC_BUNDLE_ID is required only for App Store Server API calls — Apple rejects a Server API token without a bid claim.

Tools

Tool

Purpose

asc_status

Verify credentials, report reachability and the remaining rate-limit budget. Run first when anything fails — it separates a bad key from a bad request.

asc_search_endpoints

Search both APIs by keyword, method, tag or risk tier. Returns operationIds and says which tool each belongs to.

asc_describe_endpoint

Parameters, request-body schema with real field names, risk tier.

asc_call

Reads. Path and query parameters, pagination, both APIs.

asc_write

Everything that changes data. Confirmation, dry_run, both APIs.

Reads and writes are separate tools because Claude Code ignores the standard destructiveHint annotation but honours _meta["anthropic/requiresUserInteraction"] — and that flag is per-tool. A single dispatcher could not vary it per operation. asc_write carries it, so a write prompts the user even under bypassPermissions. That is a stronger guarantee than the in-process gate, which --no-confirm can switch off.

Resources

Reference material the model can pull in deliberately, via @asc::

Resource

Contents

asc://cookbook

Cases where Apple returns a successful response meaning something other than it appears — pagination, alpha-3 territories, rejected sort, gzipped reports

asc://enums

All 90 enumerated fields, generated from Apple's spec so they cannot go stale

asc://risk

What each risk tier means and how reversible it is

asc://sources

Where each API description came from, and when

asc-response://…

Overflow storage — see below

A result too large to return inline is not cut off. The list is trimmed to what fits, the truncation is stated along with how to narrow the request, and the complete response is kept as a resource the client can read without spending context. Cutting serialised JSON mid-structure hands the model something unparseable; cutting silently is worse, because a partial list reads as a complete one.

Prompts

Four workflows, available as /mcp__asc__<name>:

release-readiness · pricing-audit · review-triage · testflight-status

Each chains several calls — a slash command wrapping one request is a synonym, not a workflow — and each encodes the traps, such as sort being rejected on customerReviews and review text being untrusted input.

Write safety

An HTTP method is a poor proxy for consequence: PATCH /v1/subscriptionPrices and PATCH /v1/appInfos/{id} are both writes, but only one changes what customers are charged, and neither is undone by repeating it. Operations carry a risk tier:

Tier

Count

Meaning

READ

797

No change.

WRITE

238

Changes data.

REVENUE

61

Pricing, subscriptions, entitlements.

DESTRUCTIVE

132

Deletes.

RELEASE

12

Builds, submissions, what ships.

ACCESS

12

Who can reach the account.

INFRASTRUCTURE

11

Certificates, identifiers, callback URLs.

By default the bottom five tiers return a confirmation token instead of executing. The token is bound by hash to the exact operation, path, query and body, so it cannot be obtained for a cheap call and spent on an expensive one. It is single-use and expires in five minutes.

--read-only    block every write        --confirm     confirm every write
--no-confirm   never confirm            (default)     confirm the five tiers above

When the client supports elicitation, asc_write asks the person directly, showing the method, path, body and tier. Otherwise it falls back to a confirmation token bound by hash to the exact operation, path, query and body, so a token issued for a cheap call cannot be spent on an expensive one. A client that declares elicitation but fails to serve it falls back rather than sailing through. dry_run reports the exact request without sending it.

Transports

node dist/index.js                       # stdio (default)
node dist/index.js --transport http --http-token "$(openssl rand -hex 32)"

HTTP binds to 127.0.0.1 and refuses to start without a bearer token. This process holds a key that can change App Store pricing; it should not listen unauthenticated. Binding off-loopback warns and is best paired with a TLS-terminating proxy or an SSH tunnel.

Keeping up with Apple

npm run fetch:specs   # re-download both descriptions
npm run build         # recompile the operation index
npm run verify        # drift check + live calls against both APIs

The two APIs are sourced differently, of necessity:

  • App Store Connect — Apple publishes a real OpenAPI 3.0 document. It is downloaded and compiled into a slim index (360KB, against a 3.3MB spec) so search stays fast and the full document is opened only to describe one operation.

  • App Store Server — Apple publishes no OpenAPI document; the documentation is prose. The authoritative machine-readable description is Apple's own client, apple/app-store-server-library-node, where every endpoint is a literal makeRequest call. fetch:specs parses the endpoint set out of that source at a pinned release tag, and verify diffs it against the catalogue in src/storekit.ts.

Two details in that catalogue contradict what the documentation implies, and both are load-bearing:

  • The hosts are api.storekit.apple.com / api.storekit-sandbox.apple.com. The older api.storekit.itunes.apple.com names no longer serve this API.

  • The mass renewal-extension status path orders its segments {productId}/{requestIdentifier} — not the reverse.

Verification

npm run verify is read-only and makes real calls. Last run:

1. Catalogue drift — src/storekit.ts vs Apple’s client
  ✓ all 30 Apple endpoints present in the catalogue
  ✓ no endpoints in the catalogue that Apple does not define

2. App Store Connect API — live
  ✓ apps_getCollection → 2 apps
  ✓ apps_getInstance / builds / appStoreVersions → HTTP 200
  ✓ pagination walked 3 pages
  ✓ bogus id → structured 404

3. App Store Server API (StoreKit 2) — live
  ✓ storekit token carries bid;  connect token correctly omits it
  ✓ getTransactionInfo / getAllSubscriptionStatuses / getTransactionHistory v2
      → authenticated and routed (Apple errorCode 4000006)
  ✓ getNotificationHistory (30d window) → HTTP 200

14 passed, 0 failed

StoreKit probes use a deliberately invalid transaction ID. The signal is the shape of the reply: a structured Apple errorCode proves the request was authenticated and routed, where a 401 would prove it was not.

Robustness

  • Timeouts and retries. Reads retry on 408/429/5xx; writes retry only on 429, where Apple rejected the request before processing it. A write that fails ambiguously is reported as ambiguous and never resent — a duplicated POST is worse than a reported failure.

  • Rate limiting. Paced against both the documented hourly limit and the undocumented per-minute one, and corrected from Apple's own x-rate-limit header, which accounts for other clients sharing the key. x-request-id is surfaced for Apple support.

  • Host pinning. Every URL, including the links.next pagination cursor, is checked against an allowlist of Apple's three API hosts. A cursor is server-supplied input; following one blindly would walk a bearer token to whatever host it names.

  • Response shaping. links and links-only relationships are stripped, links.next preserved — over 60% smaller on a real price-point listing.

  • Lifecycle. The stdio server exits on stdin EOF and on signals, rather than lingering as an orphan holding a signing key.

Known limits

  • JWS responses are decoded, not verified. StoreKit payloads arrive signed by Apple; verifying the chain needs Apple's root certificates. Decoded values appear in *_decoded fields and are labelled unverified. Do not treat them as proof of purchase without checking the signature.

  • Risk tiers are pattern-matched from method and path. They are deliberately cautious, but read asc_describe_endpoint before a write rather than trusting the tier alone.

  • Keychain storage is macOS-only. Elsewhere, use a file path with restrictive permissions.

  • --no-confirm disables the gate entirely. It exists for CI; it is a poor default for an interactive agent.

Licence

MIT

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Let ChatGPT, Claude & Cursor use your Mac: email, calendar, iMessage, Teams, files. Local, free.

  • Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.

  • Manage your NanoCart store from any AI agent: products, orders, coupons, subscribers, reports.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf-studios/app-store-connect-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server