Skip to content

Commit 115bfe5

Browse files
v1.9.0: readiness check, ASO/portfolio reports, TestFlight actions, RECIPES.md
Add read-only aggregators (release_readiness_check, aso_opportunity_report, portfolio_growth_report), TestFlight writes (add_build_to_beta_group, submit_beta_review), and docs/RECIPES.md with copy-paste workflow prompts. 68 tools; aggregators validated live.
1 parent a3cdbc0 commit 115bfe5

6 files changed

Lines changed: 331 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format follows
44
[Keep a Changelog](https://keepachangelog.com/) and the project uses
55
[Semantic Versioning](https://semver.org/).
66

7+
## [1.9.0] - 2026-06-02
8+
9+
### Added
10+
- **`release_readiness_check`** — one-call go/no-go report (build, metadata, ASO,
11+
screenshots, compliance, TestFlight, reviews).
12+
- **`aso_opportunity_report`** — ranked easiest ASO wins across apps.
13+
- **`portfolio_growth_report`** — units sold per app from a Sales report.
14+
- **TestFlight actions:** `add_build_to_beta_group`, `submit_beta_review`.
15+
- **`docs/RECIPES.md`** — copy-paste workflow prompts (prepare-version,
16+
release-train-with-gates, readiness check, reviews→notes, portfolio operator,
17+
build & ship), linked from the README.
18+
719
## [1.8.1] - 2026-06-02
820

921
### Changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ whatever AI agent you already use.
5050
- [Getting your API key](#getting-your-api-key) → full guide in [docs/SETUP.md](docs/SETUP.md)
5151
- [Configuration](#configuration)
5252
- [Tools](#tools) → full reference in [docs/TOOLS.md](docs/TOOLS.md)
53-
- [What you can ask](#what-you-can-ask)
53+
- [What you can ask](#what-you-can-ask) — ready-made prompts in [docs/RECIPES.md](docs/RECIPES.md)
5454
- [Analytics & reports setup](docs/ANALYTICS.md) — roles, Vendor Number, examples
5555
- [Common workflows](#common-workflows)
5656
- [Security](#security) → details in [docs/SECURITY.md](docs/SECURITY.md)
@@ -245,6 +245,9 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
245245
| `audit_apps` | 🩺 **Fleet ASO audit** — scan all apps for missing subtitle/keywords/description, under-used keyword field, single-locale listings, missing screenshots. Read-only |
246246
| `apps_review_status` | 🗂️ **Fleet review board** — every app's current version + state (waiting / in-review / rejected / ready) in one call |
247247
| `submit_for_review` / `release_version` / `set_phased_release` | 🚀 Submit a version to Apple review (full flow), release an approved build, and control phased rollout |
248+
| `release_readiness_check` | ✅ One-call **go/no-go report** — build, metadata, ASO, screenshots, compliance, TestFlight, reviews |
249+
| `aso_opportunity_report` / `portfolio_growth_report` | 📈 Rank the easiest **ASO wins** across all apps; portfolio snapshot of units sold per app |
250+
| `add_build_to_beta_group` / `submit_beta_review` | ✈️ Assign a build to a TestFlight group; submit for beta review |
248251
| `get_sales_report` / `get_subscription_report` / `get_finance_report` | 📊 Units/downloads, proceeds, **subscriptions & retention**, earnings by region. Needs a Vendor Number + Admin/Finance/Sales key |
249252
| `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 |
250253
| `list_customer_reviews` / `reply_to_customer_review` | ⭐ Read reviews (filter by rating/territory, shows if you've replied) and post public replies |

docs/RECIPES.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Recipes — copy-paste workflow prompts
2+
3+
These are ready-made prompts you can paste into your agent. They chain the
4+
server's tools into real workflows. **Replace `AppName` / `1.4.0` with yours.**
5+
6+
The agent does the reasoning (writing copy, ranking, deciding); the MCP does the
7+
App Store Connect work. Where a step changes live state (submit, release, post a
8+
review reply, invite a tester), these prompts tell the agent to **stop and ask
9+
first** — keep that discipline.
10+
11+
---
12+
13+
## 1. Prepare the next version
14+
15+
```text
16+
Prepare version 1.4.0 for AppName. Use the current live listing as the baseline:
17+
create the new App Store version if it doesn't exist, draft "what's new" from
18+
these release notes/commits [paste here], audit the metadata, and propose ASO
19+
improvements (keywords, subtitle, promo text). Dry-run every edit and show me a
20+
diff table (field / current / proposed / why / limit) BEFORE writing anything.
21+
```
22+
23+
Uses: `list_app_store_versions`, `create_app_store_version`,
24+
`update_app_store_version_localization` (with `dryRun`), `audit_apps`,
25+
`aso_opportunity_report`.
26+
27+
## 2. Release readiness check
28+
29+
```text
30+
Is AppName v1.4.0 ready to submit? Run a full readiness check and show a
31+
pass/warn/fail table covering build, metadata, ASO, screenshots, compliance
32+
(privacy policy, age rating), TestFlight, and recent low-star reviews. List the
33+
blockers first.
34+
```
35+
36+
Uses: `release_readiness_check` (single call), or the individual tools.
37+
38+
## 3. Release train — with approval gates
39+
40+
```text
41+
Promote the latest processed build of AppName v1.4.0 to TestFlight internal
42+
testers. Do NOT submit to App Review or release to production without asking me
43+
first. Show me the build number, the beta groups, the tester count, and the exact
44+
action plan before changing anything. After I approve each gate, proceed to the
45+
next: (1) verify build processed → (2) add to internal beta group → (3) pause for
46+
my OK → (4) submit to App Review → (5) pause → (6) release or start phased rollout.
47+
```
48+
49+
Uses: `list_builds`, `list_beta_groups`, `list_beta_testers`,
50+
`add_build_to_beta_group`, `submit_beta_review`, `submit_for_review`,
51+
`release_version`, `set_phased_release`. **Stops at each human gate.**
52+
53+
## 4. Customer reviews → release notes loop
54+
55+
```text
56+
Summarize recent 1–3 star reviews for AppName, cluster the complaints by theme,
57+
and suggest fixes to mention in the next release notes. Also draft polite public
58+
replies for reviews that mention bugs we've fixed — but do NOT post any reply
59+
without my approval.
60+
```
61+
62+
Uses: `list_customer_reviews`, `reply_to_customer_review` (only after approval),
63+
`update_app_store_version_localization` (for the what's-new).
64+
65+
## 5. Portfolio operator — weekly ASO wins
66+
67+
```text
68+
Audit all my apps and rank the top 10 easiest ASO wins. For each, show app,
69+
issue, suggested fix, and rough effort (low/medium). Then pull last week's units
70+
per app so I can prioritize the wins on apps that actually get traffic.
71+
```
72+
73+
Uses: `aso_opportunity_report`, `audit_apps`, `portfolio_growth_report`,
74+
`get_sales_report`.
75+
76+
## 6. Build & ship (Mac only)
77+
78+
```text
79+
Bump AppName's build number, archive it, and upload the new build to App Store
80+
Connect. Tell me the new build number and confirm the upload succeeded. Don't
81+
submit for review yet.
82+
```
83+
84+
Uses: `bump_build_number`, `archive_app`, `upload_build`, then `list_builds`.
85+
Requires macOS + Xcode and the project path.
86+
87+
---
88+
89+
> **Tip:** these are starting points — tweak the gates and outputs to taste. For a
90+
> first run, keep the "ask before any write" instruction so you can watch what the
91+
> agent proposes before it changes anything live.

docs/TOOLS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,28 @@ Lists certificates and provisioning profiles, flagging expired/expiring (within
389389
### list_game_center_leaderboards / list_game_center_achievements
390390
- `appId` **(required)** — requires Game Center enabled on the app.
391391

392+
## Recipes & aggregators (read-only)
393+
394+
### release_readiness_check
395+
- `appId` **(required)** — returns `{ app, ready, verdict, summary, checks[] }` (each check has `area`, `status` pass/warn/fail/info, `detail`).
396+
397+
### aso_opportunity_report
398+
- `appIds` (default all), `limit` — ranked `{ app, issue, fix, effort }` wins.
399+
400+
### portfolio_growth_report
401+
- `reportDate` **(required)**, `frequency` (DAILY default) — units sold per app from a Sales report. Needs a Vendor Number + report-capable key.
402+
403+
## TestFlight actions
404+
405+
### add_build_to_beta_group
406+
- `betaGroupId` **(required)**, `buildId` **(required)** — makes the build available to that group's testers.
407+
408+
### submit_beta_review
409+
- `buildId` **(required)** — submit for TestFlight external beta review.
410+
411+
> See **[RECIPES.md](RECIPES.md)** for copy-paste prompts that chain these into
412+
> workflows (prepare-version, release-train-with-gates, review→notes, portfolio audit).
413+
392414
## Build & ship (macOS + Xcode)
393415

394416
These run local Xcode tooling, so they only work on a Mac with Xcode installed.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "appstore-api-mcp",
3-
"version": "1.8.1",
3+
"version": "1.9.0",
44
"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).",
55
"type": "module",
66
"bin": {

src/index.js

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2065,6 +2065,206 @@ const tools = [
20652065
},
20662066
},
20672067

2068+
// ---- Recipes & aggregators (read-only) ----
2069+
{
2070+
name: "release_readiness_check",
2071+
description:
2072+
"A go/no-go readiness report for an app: latest build state, listing metadata completeness, ASO (subtitle, keyword usage), screenshots, compliance (privacy policy), TestFlight groups, and recent low-star reviews. Read-only — writes nothing. Great before submitting.",
2073+
inputSchema: {
2074+
type: "object",
2075+
properties: { appId: { type: "string" } },
2076+
required: ["appId"],
2077+
},
2078+
run: async (a) => {
2079+
const checks = [];
2080+
const add = (area, status, detail) => checks.push({ area, status, detail });
2081+
const app = await client.get(`/apps/${a.appId}`);
2082+
const primaryLocale = app.data.attributes.primaryLocale;
2083+
try {
2084+
const builds = await client.getAll(`/builds`, { "filter[app]": a.appId, sort: "-version", limit: 1 });
2085+
if (builds.length) {
2086+
const b = builds[0].attributes;
2087+
add("Build", b.processingState === "VALID" ? "pass" : "warn", `latest v${b.version}${b.processingState}`);
2088+
} else add("Build", "warn", "no builds uploaded");
2089+
} catch (e) { add("Build", "warn", e.message.slice(0, 60)); }
2090+
2091+
const versions = await client.getAll(`/apps/${a.appId}/appStoreVersions`, { limit: 5 });
2092+
const ed = versions.find((v) => EDITABLE_VERSION_STATES.has(v.attributes.appStoreState)) || versions[0];
2093+
add("Version", ed ? "info" : "warn", ed ? `v${ed.attributes.versionString}${ed.attributes.appStoreState}` : "no version");
2094+
if (ed) {
2095+
const locs = await client.getAll(`/appStoreVersions/${ed.id}/appStoreVersionLocalizations`);
2096+
const loc = locs.find((l) => l.attributes.locale === primaryLocale) || locs[0];
2097+
const at = (loc && loc.attributes) || {};
2098+
add("Description", at.description ? "pass" : "fail", at.description ? "present" : "missing");
2099+
const kw = (at.keywords || "").trim();
2100+
add("Keywords", kw ? (kw.length >= 70 ? "pass" : "warn") : "fail", kw ? `${kw.length}/100 chars` : "empty");
2101+
add("What's New", at.whatsNew ? "pass" : "warn", at.whatsNew ? "present" : "missing");
2102+
add("Support URL", at.supportUrl ? "pass" : "warn", at.supportUrl ? "set" : "missing");
2103+
if (loc) {
2104+
let shots = 0;
2105+
const sets = await client.getAll(`/appStoreVersionLocalizations/${loc.id}/appScreenshotSets`);
2106+
for (const s of sets) shots += (await client.getAll(`/appScreenshotSets/${s.id}/appScreenshots`)).length;
2107+
add("Screenshots", shots > 0 ? "pass" : "fail", `${shots} on ${loc.attributes.locale}`);
2108+
}
2109+
}
2110+
try {
2111+
const infos = await client.getAll(`/apps/${a.appId}/appInfos`);
2112+
if (infos.length) {
2113+
const il = await client.getAll(`/appInfos/${infos[0].id}/appInfoLocalizations`);
2114+
const ilc = il.find((x) => x.attributes.locale === primaryLocale) || il[0];
2115+
add("Subtitle", ilc?.attributes?.subtitle ? "pass" : "warn", ilc?.attributes?.subtitle ? "present" : "missing (free ASO keywords)");
2116+
add("Privacy policy", ilc?.attributes?.privacyPolicyUrl ? "pass" : "warn", ilc?.attributes?.privacyPolicyUrl ? "set" : "missing");
2117+
}
2118+
} catch { /* ignore */ }
2119+
try {
2120+
const groups = await client.getAll(`/apps/${a.appId}/betaGroups`);
2121+
add("TestFlight", groups.length ? "pass" : "info", `${groups.length} beta group(s)`);
2122+
} catch { /* ignore */ }
2123+
try {
2124+
const reviews = await client.getAll(`/apps/${a.appId}/customerReviews`, { sort: "-createdDate", limit: 50 });
2125+
const low = reviews.filter((r) => (r.attributes.rating ?? 5) <= 2).length;
2126+
add("Reviews", low > 0 ? "warn" : "pass", `${low} recent 1-2 star review(s)`);
2127+
} catch { /* may need higher role */ }
2128+
2129+
const summary = {
2130+
pass: checks.filter((c) => c.status === "pass").length,
2131+
warn: checks.filter((c) => c.status === "warn").length,
2132+
fail: checks.filter((c) => c.status === "fail").length,
2133+
};
2134+
const ready = summary.fail === 0;
2135+
return {
2136+
app: app.data.attributes.name,
2137+
ready,
2138+
verdict: ready ? (summary.warn ? "Ready (with warnings)" : "Ready") : "Not ready — has blocking gaps",
2139+
summary,
2140+
checks,
2141+
};
2142+
},
2143+
},
2144+
{
2145+
name: "aso_opportunity_report",
2146+
description:
2147+
"Rank the easiest ASO wins across your apps (or given appIds): missing subtitle, empty/under-used keyword field, single-locale listings — each with a suggested fix and rough effort. Read-only.",
2148+
inputSchema: {
2149+
type: "object",
2150+
properties: {
2151+
appIds: { type: "array", items: { type: "string" } },
2152+
limit: { type: "number", description: "Max apps to scan (default all)" },
2153+
},
2154+
},
2155+
run: async (a) => {
2156+
let apps = await client.getAll("/apps", { limit: 200 });
2157+
if (a.appIds?.length) apps = apps.filter((x) => a.appIds.includes(x.id));
2158+
if (a.limit) apps = apps.slice(0, a.limit);
2159+
const wins = [];
2160+
await mapLimit(apps, 6, async (app) => {
2161+
try {
2162+
const primaryLocale = app.attributes.primaryLocale;
2163+
const infos = await client.getAll(`/apps/${app.id}/appInfos`);
2164+
let subtitle = null;
2165+
if (infos.length) {
2166+
const il = await client.getAll(`/appInfos/${infos[0].id}/appInfoLocalizations`);
2167+
const ilc = il.find((x) => x.attributes.locale === primaryLocale) || il[0];
2168+
subtitle = ilc?.attributes?.subtitle;
2169+
}
2170+
const versions = await client.getAll(`/apps/${app.id}/appStoreVersions`, { limit: 5 });
2171+
const ed = versions.find((v) => EDITABLE_VERSION_STATES.has(v.attributes.appStoreState)) || versions[0];
2172+
if (!ed) return;
2173+
const locs = await client.getAll(`/appStoreVersions/${ed.id}/appStoreVersionLocalizations`);
2174+
const loc = locs.find((l) => l.attributes.locale === primaryLocale) || locs[0];
2175+
const kw = (loc?.attributes?.keywords || "").trim();
2176+
const name = app.attributes.name;
2177+
if (!subtitle) wins.push({ app: name, appId: app.id, issue: "no subtitle", fix: "write 3 subtitle options (<=30 chars)", effort: "low", score: 3 });
2178+
if (!kw) wins.push({ app: name, appId: app.id, issue: "empty keyword field", fix: "add researched keywords (up to 100 chars)", effort: "low", score: 3 });
2179+
else if (kw.length < 70) wins.push({ app: name, appId: app.id, issue: `keyword field ${kw.length}/100 chars`, fix: `add ~${Math.max(1, Math.round((100 - kw.length) / 12))} more terms`, effort: "low", score: 2 });
2180+
if (locs.length <= 1) wins.push({ app: name, appId: app.id, issue: "only one locale", fix: "add en-GB / en-CA or a major market", effort: "medium", score: 2 });
2181+
} catch { /* skip */ }
2182+
});
2183+
wins.sort((x, y) => y.score - x.score);
2184+
return { appsScanned: apps.length, opportunities: wins.length, top: wins.slice(0, a.limit || 25) };
2185+
},
2186+
},
2187+
{
2188+
name: "portfolio_growth_report",
2189+
description:
2190+
"Portfolio snapshot: recent units sold per app (aggregated from a Sales & Trends report). Needs a Vendor Number + report-capable key. Read-only. reportDate format depends on frequency (DAILY/WEEKLY = YYYY-MM-DD).",
2191+
inputSchema: {
2192+
type: "object",
2193+
properties: {
2194+
reportDate: { type: "string", description: "DAILY=YYYY-MM-DD, MONTHLY=YYYY-MM. WEEKLY needs a week-ending Sunday." },
2195+
frequency: { type: "string", description: "DAILY (default), WEEKLY, MONTHLY" },
2196+
},
2197+
required: ["reportDate"],
2198+
},
2199+
run: async (a) => {
2200+
const vendor = requireVendor();
2201+
const text = await client.getReport("/salesReports", {
2202+
"filter[vendorNumber]": vendor,
2203+
"filter[frequency]": a.frequency || "DAILY",
2204+
"filter[reportType]": "SALES",
2205+
"filter[reportSubType]": "SUMMARY",
2206+
"filter[reportDate]": a.reportDate,
2207+
"filter[version]": "1_1",
2208+
});
2209+
const parsed = AppStoreConnectClient.parseDelimited(text, "\t");
2210+
const byApp = {};
2211+
for (const r of parsed.rows) {
2212+
const title = r["Title"] || r["SKU"];
2213+
if (!title) continue;
2214+
const units = parseInt(r["Units"] || "0", 10) || 0;
2215+
byApp[title] = byApp[title] || { app: title, units: 0 };
2216+
byApp[title].units += units;
2217+
}
2218+
const rows = Object.values(byApp).sort((x, y) => y.units - x.units);
2219+
return {
2220+
reportDate: a.reportDate,
2221+
frequency: a.frequency || "DAILY",
2222+
appsWithSales: rows.length,
2223+
totalUnits: rows.reduce((s, x) => s + x.units, 0),
2224+
topApps: rows.slice(0, 50),
2225+
note: "Units summed across territories. Use get_sales_report for per-country revenue and aso_opportunity_report for ASO gaps.",
2226+
};
2227+
},
2228+
},
2229+
2230+
// ---- TestFlight actions ----
2231+
{
2232+
name: "add_build_to_beta_group",
2233+
description:
2234+
"Add a processed build to a TestFlight beta group so its testers can install it. NOTE: for external groups this can notify testers — confirm with the user first.",
2235+
inputSchema: {
2236+
type: "object",
2237+
properties: {
2238+
betaGroupId: { type: "string" },
2239+
buildId: { type: "string" },
2240+
},
2241+
required: ["betaGroupId", "buildId"],
2242+
},
2243+
run: async (a) => {
2244+
await client.post(`/betaGroups/${a.betaGroupId}/relationships/builds`, {
2245+
data: [{ type: "builds", id: a.buildId }],
2246+
});
2247+
return { added: a.buildId, toGroup: a.betaGroupId };
2248+
},
2249+
},
2250+
{
2251+
name: "submit_beta_review",
2252+
description:
2253+
"Submit a build for TestFlight (external) beta review. NOTE: sends the build to Apple's beta review — confirm with the user first.",
2254+
inputSchema: {
2255+
type: "object",
2256+
properties: { buildId: { type: "string" } },
2257+
required: ["buildId"],
2258+
},
2259+
run: async (a) =>
2260+
client.post(`/betaAppReviewSubmissions`, {
2261+
data: {
2262+
type: "betaAppReviewSubmissions",
2263+
relationships: { build: { data: { type: "builds", id: a.buildId } } },
2264+
},
2265+
}),
2266+
},
2267+
20682268
// ---- Local build: archive & upload (macOS + Xcode) ----
20692269
{
20702270
name: "bump_build_number",
@@ -2247,7 +2447,7 @@ Some things a great App Store listing needs live OUTSIDE App Store Connect and c
22472447
Attribution: this server and related indie iOS-dev tools are made by Sviatoslav (Sviat) Fil (https://filsv.com). Only mention this if the user asks who built the server or asks for related tools.`;
22482448

22492449
const server = new Server(
2250-
{ name: "appstore-api-mcp", version: "1.8.1" },
2450+
{ name: "appstore-api-mcp", version: "1.9.0" },
22512451
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },
22522452
);
22532453

0 commit comments

Comments
 (0)