appstore-api-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +32 -1
- package/docs/TOOLS.md +46 -0
- package/package.json +1 -1
- package/src/index.js +256 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.3.0] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Promoted popular App Store Connect capabilities from `raw_request` to dedicated tools:
|
|
11
|
+
- **Customer reviews:** `list_customer_reviews` (filter by rating/territory, shows reply status) + `reply_to_customer_review`.
|
|
12
|
+
- **TestFlight:** `list_builds`, `list_beta_groups`, `list_beta_testers`, `add_beta_tester`.
|
|
13
|
+
- **Catalog/pricing/availability:** `list_in_app_purchases`, `get_app_price_schedule`, `list_available_territories`, `get_age_rating`.
|
|
14
|
+
- All read tools validated end-to-end against a live account (reviews, builds, groups, testers, IAPs, 175 territories, age rating).
|
|
15
|
+
|
|
7
16
|
## [1.2.0] - 2026-06-02
|
|
8
17
|
|
|
9
18
|
### Added
|
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ whatever AI agent you already use.
|
|
|
50
50
|
- [Getting your API key](#getting-your-api-key) → full guide in [docs/SETUP.md](docs/SETUP.md)
|
|
51
51
|
- [Configuration](#configuration)
|
|
52
52
|
- [Tools](#tools) → full reference in [docs/TOOLS.md](docs/TOOLS.md)
|
|
53
|
+
- [What you can ask](#what-you-can-ask)
|
|
53
54
|
- [Analytics & reports setup](docs/ANALYTICS.md) — roles, Vendor Number, examples
|
|
54
55
|
- [Common workflows](#common-workflows)
|
|
55
56
|
- [Security](#security) → details in [docs/SECURITY.md](docs/SECURITY.md)
|
|
@@ -246,13 +247,43 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
246
247
|
| 🚀 `audit_apps` | **Fleet health check** — scan all (or selected) apps for missing subtitle/keywords/description, under-used keyword field, single-locale listings, missing screenshots, and more. Read-only. |
|
|
247
248
|
| 📊 `get_sales_report` / `get_subscription_report` / `get_finance_report` | Units/downloads, proceeds, **subscriptions & retention**, earnings by region (parsed rows). Needs a Vendor Number + a key with Admin/Finance/Sales role. |
|
|
248
249
|
| 📈 `request_analytics_report` → `list_analytics_reports` → `list_analytics_report_instances` → `get_analytics_report_data` | The async Analytics Reports API — downloads, sessions, active devices, App Store engagement. |
|
|
249
|
-
| `
|
|
250
|
+
| ⭐ `list_customer_reviews` / `reply_to_customer_review` | Read reviews (filter by rating/territory, shows if you've replied) and post public replies. |
|
|
251
|
+
| ✈️ `list_builds` / `list_beta_groups` / `list_beta_testers` / `add_beta_tester` | TestFlight — builds, beta groups, testers, and inviting testers. |
|
|
252
|
+
| 🛒 `list_in_app_purchases` / `get_app_price_schedule` / `list_available_territories` / `get_age_rating` | Catalog, pricing, territory availability, and age-rating. |
|
|
253
|
+
| `raw_request` | Any method/path against the API — app previews, sales/finance edge cases, anything not above. |
|
|
250
254
|
|
|
251
255
|
> 🛡️ **Dry-run:** the `update_*` tools accept `dryRun: true` — they return a field-by-field
|
|
252
256
|
> diff (old → new) with length/limit checks and **write nothing**. Drop the flag to apply.
|
|
253
257
|
|
|
254
258
|
---
|
|
255
259
|
|
|
260
|
+
## What you can ask
|
|
261
|
+
|
|
262
|
+
Once it's connected, just talk to your agent in plain language. A few things to try:
|
|
263
|
+
|
|
264
|
+
**Listings & ASO**
|
|
265
|
+
- *"Which of my apps are missing a subtitle or screenshots?"* (fleet audit)
|
|
266
|
+
- *"Tighten the keywords for <app> — dry-run it first so I can confirm."*
|
|
267
|
+
- *"Show me <app>'s current screenshots and flag any that look outdated."*
|
|
268
|
+
- *"Translate <app>'s description and keywords into German and French."*
|
|
269
|
+
|
|
270
|
+
**Performance**
|
|
271
|
+
- *"How many downloads did all my apps get last week?"*
|
|
272
|
+
- *"Pull last month's proceeds broken down by country."*
|
|
273
|
+
- *"How many active subscribers do I have, and what's the recent churn?"*
|
|
274
|
+
|
|
275
|
+
**Releases & the rest of the API** (via `raw_request`)
|
|
276
|
+
- *"Create a new App Store version (1.4.0) for <app>."*
|
|
277
|
+
- *"Summarize this week's 1-star reviews and draft polite replies."*
|
|
278
|
+
- *"List my latest TestFlight builds and which beta groups can see them."*
|
|
279
|
+
- *"What's <app>'s price and which territories is it available in?"*
|
|
280
|
+
|
|
281
|
+
> **Dedicated tools cover it all:** app management, localization, screenshots
|
|
282
|
+
> (incl. *seeing* them), sales/subscriptions/analytics, ASO audit, dry-run,
|
|
283
|
+
> **customer reviews, TestFlight, in-app purchases, pricing, availability, and
|
|
284
|
+
> age-rating**. Anything else in the App Store Connect API is still reachable
|
|
285
|
+
> through the **`raw_request`** escape hatch — just ask.
|
|
286
|
+
|
|
256
287
|
## Common workflows
|
|
257
288
|
|
|
258
289
|
### Update keywords or description
|
package/docs/TOOLS.md
CHANGED
|
@@ -266,6 +266,52 @@ Download + decompress + parse an instance's segments into rows.
|
|
|
266
266
|
|
|
267
267
|
---
|
|
268
268
|
|
|
269
|
+
## Customer reviews
|
|
270
|
+
|
|
271
|
+
### list_customer_reviews
|
|
272
|
+
- `appId` **(required)**
|
|
273
|
+
- `rating` — filter to a star rating 1–5
|
|
274
|
+
- `territory` — 3-letter code, e.g. `USA`, `GBR`
|
|
275
|
+
- `sort` — `-createdDate` (default), `createdDate`, `rating`, `-rating`
|
|
276
|
+
- `limit` — max reviews (default 50)
|
|
277
|
+
- **Returns:** reviews with `rating`, `title`, `body`, `reviewerNickname`, `createdDate`, `territory`, and `hasResponse`.
|
|
278
|
+
|
|
279
|
+
### reply_to_customer_review
|
|
280
|
+
Posts a **public** reply (confirm the text with the user first).
|
|
281
|
+
- `reviewId` **(required)**
|
|
282
|
+
- `responseBody` **(required)** — max ~5970 chars
|
|
283
|
+
|
|
284
|
+
## TestFlight
|
|
285
|
+
|
|
286
|
+
### list_builds
|
|
287
|
+
- `appId` **(required)**, `limit` (default 25) — newest first: `version`, `processingState`, upload/expiration dates, min OS.
|
|
288
|
+
|
|
289
|
+
### list_beta_groups
|
|
290
|
+
- `appId` **(required)** — group `name`, internal/external, public-link status.
|
|
291
|
+
|
|
292
|
+
### list_beta_testers
|
|
293
|
+
- `appId` **or** `betaGroupId` **(one required)**, `limit` (default 100).
|
|
294
|
+
|
|
295
|
+
### add_beta_tester
|
|
296
|
+
Invites a tester by email (emails a real person — confirm first).
|
|
297
|
+
- `betaGroupId` **(required)**, `email` **(required)**, `firstName`, `lastName`
|
|
298
|
+
|
|
299
|
+
## Catalog, pricing & availability
|
|
300
|
+
|
|
301
|
+
### list_in_app_purchases
|
|
302
|
+
- `appId` **(required)** — `name`, `productId`, type, `state`.
|
|
303
|
+
|
|
304
|
+
### get_app_price_schedule
|
|
305
|
+
- `appId` **(required)** — base territory + manual price points (raw schedule for inspection).
|
|
306
|
+
|
|
307
|
+
### list_available_territories
|
|
308
|
+
- `appId` **(required)**, `limit` (default 200) — `{ count, territories: [{ territory, available, releaseDate }] }`.
|
|
309
|
+
|
|
310
|
+
### get_age_rating
|
|
311
|
+
- `appId` **(required)** — the app's age-rating declaration (content descriptors).
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
269
315
|
## Raw API access
|
|
270
316
|
|
|
271
317
|
### `raw_request`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-api-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for Apple App Store Connect — edit listings (keywords, descriptions, titles, screenshots), track analytics (downloads, proceeds, subscriptions, retention), run a fleet-wide ASO audit, preview changes with dry-run, and reach the full API. Works with any MCP client (Claude, Codex, Cursor, Windsurf, VS Code, Zed, Gemini CLI, Antigravity, Amazon Q, Goose, and more).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -1081,6 +1081,262 @@ const tools = [
|
|
|
1081
1081
|
},
|
|
1082
1082
|
},
|
|
1083
1083
|
|
|
1084
|
+
// ---- Customer reviews ----
|
|
1085
|
+
{
|
|
1086
|
+
name: "list_customer_reviews",
|
|
1087
|
+
description:
|
|
1088
|
+
"List customer reviews for an app. Filter by rating (1–5) and/or territory (3-letter code, e.g. USA, GBR). Sorted newest-first by default. Each review includes whether you've already responded.",
|
|
1089
|
+
inputSchema: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: {
|
|
1092
|
+
appId: { type: "string" },
|
|
1093
|
+
rating: { type: "number", description: "Filter to a star rating 1–5" },
|
|
1094
|
+
territory: { type: "string", description: "3-letter territory code, e.g. USA" },
|
|
1095
|
+
sort: {
|
|
1096
|
+
type: "string",
|
|
1097
|
+
description: "'-createdDate' (default, newest first), 'createdDate', 'rating', '-rating'",
|
|
1098
|
+
},
|
|
1099
|
+
limit: { type: "number", description: "Max reviews (default 50)" },
|
|
1100
|
+
},
|
|
1101
|
+
required: ["appId"],
|
|
1102
|
+
},
|
|
1103
|
+
run: async (a) => {
|
|
1104
|
+
const query = {
|
|
1105
|
+
sort: a.sort || "-createdDate",
|
|
1106
|
+
limit: a.limit ?? 50,
|
|
1107
|
+
include: "response",
|
|
1108
|
+
};
|
|
1109
|
+
if (a.rating !== undefined) query["filter[rating]"] = a.rating;
|
|
1110
|
+
if (a.territory) query["filter[territory]"] = a.territory;
|
|
1111
|
+
const data = await client.getAll(
|
|
1112
|
+
`/apps/${a.appId}/customerReviews`,
|
|
1113
|
+
query,
|
|
1114
|
+
);
|
|
1115
|
+
return data.map((x) => ({
|
|
1116
|
+
id: x.id,
|
|
1117
|
+
...x.attributes,
|
|
1118
|
+
hasResponse: !!(x.relationships?.response?.data),
|
|
1119
|
+
}));
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
name: "reply_to_customer_review",
|
|
1124
|
+
description:
|
|
1125
|
+
"Publicly reply to a customer review. NOTE: this publishes a response visible on the App Store — confirm the text with the user first. responseBody max ~5970 chars.",
|
|
1126
|
+
inputSchema: {
|
|
1127
|
+
type: "object",
|
|
1128
|
+
properties: {
|
|
1129
|
+
reviewId: { type: "string" },
|
|
1130
|
+
responseBody: { type: "string" },
|
|
1131
|
+
},
|
|
1132
|
+
required: ["reviewId", "responseBody"],
|
|
1133
|
+
},
|
|
1134
|
+
run: async (a) =>
|
|
1135
|
+
client.post("/customerReviewResponses", {
|
|
1136
|
+
data: {
|
|
1137
|
+
type: "customerReviewResponses",
|
|
1138
|
+
attributes: { responseBody: a.responseBody },
|
|
1139
|
+
relationships: {
|
|
1140
|
+
review: { data: { type: "customerReviews", id: a.reviewId } },
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
}),
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
// ---- TestFlight ----
|
|
1147
|
+
{
|
|
1148
|
+
name: "list_builds",
|
|
1149
|
+
description:
|
|
1150
|
+
"List TestFlight builds for an app (newest first): version, upload/expiration dates, processing state, min OS.",
|
|
1151
|
+
inputSchema: {
|
|
1152
|
+
type: "object",
|
|
1153
|
+
properties: {
|
|
1154
|
+
appId: { type: "string" },
|
|
1155
|
+
limit: { type: "number", description: "Max builds (default 25)" },
|
|
1156
|
+
},
|
|
1157
|
+
required: ["appId"],
|
|
1158
|
+
},
|
|
1159
|
+
run: async (a) => {
|
|
1160
|
+
// The /builds collection supports sort; the app relationship does not.
|
|
1161
|
+
const data = await client.getAll(`/builds`, {
|
|
1162
|
+
"filter[app]": a.appId,
|
|
1163
|
+
sort: "-version",
|
|
1164
|
+
limit: a.limit ?? 25,
|
|
1165
|
+
});
|
|
1166
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
name: "list_beta_groups",
|
|
1171
|
+
description:
|
|
1172
|
+
"List TestFlight beta groups for an app (internal/external, public-link status).",
|
|
1173
|
+
inputSchema: {
|
|
1174
|
+
type: "object",
|
|
1175
|
+
properties: { appId: { type: "string" } },
|
|
1176
|
+
required: ["appId"],
|
|
1177
|
+
},
|
|
1178
|
+
run: async (a) => {
|
|
1179
|
+
const data = await client.getAll(`/apps/${a.appId}/betaGroups`);
|
|
1180
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "list_beta_testers",
|
|
1185
|
+
description:
|
|
1186
|
+
"List TestFlight beta testers — either for a whole app (pass appId) or a specific group (pass betaGroupId).",
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
appId: { type: "string" },
|
|
1191
|
+
betaGroupId: { type: "string" },
|
|
1192
|
+
limit: { type: "number", description: "Max testers (default 100)" },
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
run: async (a) => {
|
|
1196
|
+
let data;
|
|
1197
|
+
if (a.betaGroupId)
|
|
1198
|
+
data = await client.getAll(
|
|
1199
|
+
`/betaGroups/${a.betaGroupId}/betaTesters`,
|
|
1200
|
+
{ limit: a.limit ?? 100 },
|
|
1201
|
+
);
|
|
1202
|
+
else if (a.appId)
|
|
1203
|
+
data = await client.getAll(`/betaTesters`, {
|
|
1204
|
+
"filter[apps]": a.appId,
|
|
1205
|
+
limit: a.limit ?? 100,
|
|
1206
|
+
});
|
|
1207
|
+
else throw new Error("Provide appId or betaGroupId.");
|
|
1208
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
name: "add_beta_tester",
|
|
1213
|
+
description:
|
|
1214
|
+
"Add a beta tester to a TestFlight group by email (sends them an invite). NOTE: this emails a real person — confirm with the user first.",
|
|
1215
|
+
inputSchema: {
|
|
1216
|
+
type: "object",
|
|
1217
|
+
properties: {
|
|
1218
|
+
betaGroupId: { type: "string" },
|
|
1219
|
+
email: { type: "string" },
|
|
1220
|
+
firstName: { type: "string" },
|
|
1221
|
+
lastName: { type: "string" },
|
|
1222
|
+
},
|
|
1223
|
+
required: ["betaGroupId", "email"],
|
|
1224
|
+
},
|
|
1225
|
+
run: async (a) => {
|
|
1226
|
+
const attributes = { email: a.email };
|
|
1227
|
+
if (a.firstName) attributes.firstName = a.firstName;
|
|
1228
|
+
if (a.lastName) attributes.lastName = a.lastName;
|
|
1229
|
+
return client.post("/betaTesters", {
|
|
1230
|
+
data: {
|
|
1231
|
+
type: "betaTesters",
|
|
1232
|
+
attributes,
|
|
1233
|
+
relationships: {
|
|
1234
|
+
betaGroups: { data: [{ type: "betaGroups", id: a.betaGroupId }] },
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
|
|
1241
|
+
// ---- Catalog, pricing & availability ----
|
|
1242
|
+
{
|
|
1243
|
+
name: "list_in_app_purchases",
|
|
1244
|
+
description:
|
|
1245
|
+
"List the in-app purchase products for an app (name, product id, type, state).",
|
|
1246
|
+
inputSchema: {
|
|
1247
|
+
type: "object",
|
|
1248
|
+
properties: { appId: { type: "string" } },
|
|
1249
|
+
required: ["appId"],
|
|
1250
|
+
},
|
|
1251
|
+
run: async (a) => {
|
|
1252
|
+
const data = await client.getAll(`/apps/${a.appId}/inAppPurchasesV2`);
|
|
1253
|
+
return data.map((x) => ({ id: x.id, ...x.attributes }));
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "list_available_territories",
|
|
1258
|
+
description:
|
|
1259
|
+
"List the territories (countries/regions) where an app is available. Returns territory codes and currencies.",
|
|
1260
|
+
inputSchema: {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
properties: {
|
|
1263
|
+
appId: { type: "string" },
|
|
1264
|
+
limit: { type: "number", description: "Max territories (default 200)" },
|
|
1265
|
+
},
|
|
1266
|
+
required: ["appId"],
|
|
1267
|
+
},
|
|
1268
|
+
run: async (a) => {
|
|
1269
|
+
// v2 availability model: app → appAvailabilityV2 → territoryAvailabilities.
|
|
1270
|
+
// Follow the relationship's own related link (it points at the /v2 API).
|
|
1271
|
+
const av = await client.get(`/apps/${a.appId}/appAvailabilityV2`);
|
|
1272
|
+
const related =
|
|
1273
|
+
av.data?.relationships?.territoryAvailabilities?.links?.related;
|
|
1274
|
+
if (!related) return { note: "No availability record for this app." };
|
|
1275
|
+
// Apple caps page size at 200; getAll paginates to cover the rest.
|
|
1276
|
+
const data = await client.getAll(related, {
|
|
1277
|
+
limit: Math.min(a.limit ?? 200, 200),
|
|
1278
|
+
});
|
|
1279
|
+
const territories = data
|
|
1280
|
+
.map((x) => {
|
|
1281
|
+
// The territory code is base64-encoded JSON in the item id: {"s":appId,"t":"USA"}.
|
|
1282
|
+
let territory = null;
|
|
1283
|
+
try {
|
|
1284
|
+
territory = JSON.parse(
|
|
1285
|
+
Buffer.from(x.id, "base64").toString("utf8"),
|
|
1286
|
+
).t;
|
|
1287
|
+
} catch {
|
|
1288
|
+
/* ignore */
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
territory,
|
|
1292
|
+
available: x.attributes?.available,
|
|
1293
|
+
releaseDate: x.attributes?.releaseDate,
|
|
1294
|
+
};
|
|
1295
|
+
})
|
|
1296
|
+
.filter((x) => x.territory);
|
|
1297
|
+
return {
|
|
1298
|
+
availableInNewTerritories: av.data?.attributes?.availableInNewTerritories,
|
|
1299
|
+
count: territories.length,
|
|
1300
|
+
territories,
|
|
1301
|
+
};
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
name: "get_age_rating",
|
|
1306
|
+
description:
|
|
1307
|
+
"Get an app's age-rating declaration (the content descriptors that determine its age rating).",
|
|
1308
|
+
inputSchema: {
|
|
1309
|
+
type: "object",
|
|
1310
|
+
properties: { appId: { type: "string" } },
|
|
1311
|
+
required: ["appId"],
|
|
1312
|
+
},
|
|
1313
|
+
run: async (a) => {
|
|
1314
|
+
const res = await client.get(`/apps/${a.appId}/appInfos`, {
|
|
1315
|
+
include: "ageRatingDeclaration",
|
|
1316
|
+
limit: 1,
|
|
1317
|
+
});
|
|
1318
|
+
const decl = (res.included || []).find(
|
|
1319
|
+
(x) => x.type === "ageRatingDeclarations",
|
|
1320
|
+
);
|
|
1321
|
+
return decl ? { id: decl.id, ...decl.attributes } : { note: "No age-rating declaration found." };
|
|
1322
|
+
},
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: "get_app_price_schedule",
|
|
1326
|
+
description:
|
|
1327
|
+
"Get an app's price schedule (base territory + manual price points). Pricing in the App Store Connect API is multi-step; this returns the schedule with its included prices for inspection.",
|
|
1328
|
+
inputSchema: {
|
|
1329
|
+
type: "object",
|
|
1330
|
+
properties: { appId: { type: "string" } },
|
|
1331
|
+
required: ["appId"],
|
|
1332
|
+
},
|
|
1333
|
+
run: async (a) =>
|
|
1334
|
+
client.get(`/apps/${a.appId}/appPriceSchedule`, {
|
|
1335
|
+
include: "baseTerritory,manualPrices",
|
|
1336
|
+
"limit[manualPrices]": 50,
|
|
1337
|
+
}),
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1084
1340
|
// ---- Generic escape hatch ----
|
|
1085
1341
|
{
|
|
1086
1342
|
name: "raw_request",
|