Skip to main content
Glama
ravenKaisar

App Store Connect MCP

by ravenKaisar
README.md
# App Store Connect MCP

Node.js and TypeScript MCP server for App Store Connect. It supports multiple Apple accounts and can run either as a self-hosted remote MCP gateway or as a local `npx` stdio MCP server.

## Features

- Multiple named App Store Connect accounts
- Account-prefixed MCP tools, so one server can expose several Apple accounts safely
- ES256 App Store Connect JWT generation and caching
- Remote Streamable HTTP MCP endpoint at `/mcp`
- Bearer-token protection for remote HTTP mode
- Local stdio mode for `npx` usage
- Dockerfile and Docker Compose support
- Secret-safe logging and responses
- Zod validation for account configuration and tool inputs

## Modes

| Mode | Best For | Credentials Location | MCP Transport |
| --- | --- | --- | --- |
| Remote HTTP gateway | Teams, shared servers, centralized access | Mounted server-side JSON file | Streamable HTTP |
| Local stdio package | Individual developers or local automation | MCP client environment variable | stdio |

Use remote HTTP mode when developers should not receive Apple private keys. Use local stdio mode only when the machine running the MCP client is allowed to hold Apple credentials.

## Architecture

```text
Remote HTTP mode:

MCP Client
   |
   | HTTPS /mcp
   | Authorization: Bearer server-token
   v
App Store Connect MCP Gateway
   |
   | ES256 Apple JWT
   v
App Store Connect API
```

```text
Local stdio mode:

MCP Client
   |
   | npx @kaisar/mcp-appstore-connect
   | APPLE_ACCOUNTS=base64-json
   v
Local MCP Server
   |
   | ES256 Apple JWT
   v
App Store Connect API
```

## Apple Account Configuration

Create an `apple_accounts.json` file:

```json
[
  {
    "id": "production",
    "keyId": "ABC123DEFG",
    "issuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
    "vendorNumber": "12345678"
  },
  {
    "id": "client_a",
    "keyId": "XYZ123DEFG",
    "issuerId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
    "privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
  }
]
```

Fields:

- `id`: local account name used to generate MCP tool prefixes
- `keyId`: App Store Connect API key ID
- `issuerId`: App Store Connect issuer ID
- `privateKey`: `.p8` private key content, with newlines preserved or escaped as `\n`
- `vendorNumber`: optional Sales and Trends vendor number

The config does not support `defaultBundleId` or `defaultAppStoreId`. Pass app IDs explicitly to tools.

Account IDs can contain spaces or symbols. Tool prefixes are generated by lowercasing the account ID and replacing unsafe characters with `_`. Prefixes must be unique.

Example:

```text
"Client A" -> client_a
"Production" -> production
```

## Tool Names

The server exposes one set of tools per account. Tool names use:

```text
account_prefix__tool_name
```

Examples:

```text
production__list_apps
production__get_builds
production__get_sales_data
client_a__list_beta_groups
client_a__get_customer_reviews
```

The server also exposes:

```text
list_gateway_accounts
```

This returns configured account IDs and generated tool prefixes only. It never returns Apple keys, issuer IDs, private keys, or vendor numbers.

## Remote HTTP Gateway

Remote mode runs a web server with:

- `GET /healthz`
- `POST /mcp`

Required environment variables:

```bash
APPLE_ACCOUNTS_FILE=/run/secrets/apple_accounts.json
MCP_SERVER_TOKEN=replace-with-long-random-token
```

`APPLE_ACCOUNTS_JSON` is intentionally not supported in remote HTTP mode. Use a mounted file so Apple credentials are not baked into the image or visible in process environment dumps.

### Docker Compose

Create local files:

```bash
mkdir -p secrets
cp .env.example .env
```

Add Apple credentials to:

```text
secrets/apple_accounts.json
```

Set the gateway token in `.env`:

```env
MCP_SERVER_TOKEN=replace-with-long-random-token
```

Start the server:

```bash
docker compose up --build
```

Health check:

```bash
curl http://localhost:3000/healthz
```

Unauthorized MCP requests return `401`:

```bash
curl -i -X POST http://localhost:3000/mcp
```

Manual initialize request:

```bash
curl -i http://localhost:3000/mcp \
  -H "Authorization: Bearer replace-with-long-random-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'
```

### Remote MCP Client Config

Use this pattern for clients that support remote Streamable HTTP MCP servers:

```json
{
  "servers": {
    "appstore-connect": {
      "type": "http",
      "url": "https://example.com/mcp",
      "headers": {
        "Authorization": "Bearer replace-with-long-random-token"
      }
    }
  }
}
```

Developers using remote mode only need the URL and gateway token. They do not need Apple API credentials.

## Local npx stdio

Local stdio mode runs directly from npm:

```bash
npx -y @kaisar/mcp-appstore-connect
```

It requires `APPLE_ACCOUNTS`, a base64-encoded copy of `apple_accounts.json`.

Generate the value:

```bash
node -e "process.stdout.write(Buffer.from(require('fs').readFileSync('secrets/apple_accounts.json')).toString('base64'))"
```

MCP client config:

```json
{
  "mcpServers": {
    "appstore-connect": {
      "command": "npx",
      "args": ["-y", "@kaisar/mcp-appstore-connect"],
      "env": {
        "APPLE_ACCOUNTS": "base64-encoded-apple-accounts-json"
      }
    }
  }
}
```

Base64 is not encryption. Anyone who can read this MCP client config can decode the Apple credentials.

## Development

Install dependencies:

```bash
pnpm install
```

Build:

```bash
pnpm run build
```

Run tests:

```bash
pnpm test
```

Run remote HTTP mode locally:

```bash
APPLE_ACCOUNTS_FILE="./secrets/apple_accounts.json" \
MCP_SERVER_TOKEN="replace-with-long-random-token" \
pnpm run dev
```

Run stdio mode locally:

```bash
APPLE_ACCOUNTS="base64-encoded-apple-accounts-json" pnpm run dev:stdio
```

Validate mounted-file configuration:

```bash
APPLE_ACCOUNTS_FILE="./secrets/apple_accounts.json" pnpm run validate:config
```

Validate base64 configuration:

```bash
APPLE_ACCOUNTS="base64-encoded-apple-accounts-json" pnpm run validate:config
```

Validation prints only account IDs, tool prefixes, private key validity, and key type. It does not print private key content.

## Security

- Do not commit real `apple_accounts.json` files.
- Do not commit `.env` files.
- Do not bake Apple private keys into Docker images.
- Use HTTPS for remote HTTP deployments.
- Use a long random `MCP_SERVER_TOKEN`.
- Rotate `MCP_SERVER_TOKEN` when access changes.
- Treat the base64 `APPLE_ACCOUNTS` value as secret material.
- Give App Store Connect API keys the minimum Apple role needed for the tools users will call.
- Review logs before sharing them. Logs are designed to avoid secrets, but operational logs can still reveal account IDs and request timing.

## Supported Tool Areas

The server focuses on read-oriented App Store Connect operations, including:

- apps and app metadata
- builds and TestFlight resources
- beta groups and beta feedback
- customer reviews and review summaries
- app versions, screenshots, previews, and localizations
- in-app purchases, subscriptions, offers, prices, and promoted purchases
- app availability and pricing
- analytics, sales, finance, and performance reports
- App Store Connect users


## Troubleshooting

`Set APPLE_ACCOUNTS_FILE to the mounted apple_accounts.json path`

Remote HTTP mode needs `APPLE_ACCOUNTS_FILE` and a readable mounted JSON file.

`Set APPLE_ACCOUNTS to base64-encoded apple_accounts.json content`

Local stdio mode needs `APPLE_ACCOUNTS`.

`Set MCP_SERVER_TOKEN`

Remote HTTP mode needs a gateway token.

`Unauthorized`

The MCP client bearer token does not match `MCP_SERVER_TOKEN`.

`JWT generation failed`

The private key is not a valid App Store Connect `.p8` EC private key. Re-copy the complete key, including the `BEGIN PRIVATE KEY` and `END PRIVATE KEY` lines.

Sales report errors mentioning `reportSubType` or `vendorNumber`

Sales and Trends reports require Apple report filters and a numeric Vendor Number. Pass `vendorNumber` in the tool call or configure it in the account entry.

App lookup errors for values like `com.example.app`

Most App Store Connect API app endpoints expect the numeric app resource ID returned by `list_apps`, not the bundle ID.