appstore-api-mcp 1.10.2 → 1.11.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 +1 -1
- package/docs/TOOLS.md +14 -5
- package/package.json +1 -1
- package/src/guardrails.js +1 -0
- package/src/index.js +113 -16
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.11.0] - 2026-06-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Screenshot backup & restore.** `snapshot_app_metadata` now takes
|
|
11
|
+
`includeScreenshots:true` to download the actual screenshot **images** locally,
|
|
12
|
+
and **`restore_screenshots`** re-uploads them — so deleted screenshots can be
|
|
13
|
+
brought back (use `replace:true` for a true restore). Previously only screenshot
|
|
14
|
+
*references* were stored; now the pixels can be too.
|
|
15
|
+
|
|
7
16
|
## [1.10.2] - 2026-06-02
|
|
8
17
|
|
|
9
18
|
### Changed
|
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
265
265
|
| `apps_review_status` | 🗂️ **Fleet review board** — every app's current version + state (waiting / in-review / rejected / ready) in one call |
|
|
266
266
|
| `submit_for_review` / `release_version` / `set_phased_release` | 🚀 Submit a version to Apple review (full flow), release an approved build, and control phased rollout |
|
|
267
267
|
| `doctor` | 🩺 Diagnose setup: Node, creds, key works, role capabilities, vendor number, Mac build tools, write mode |
|
|
268
|
-
| `snapshot_app_metadata` / `diff_app_metadata_snapshot` / `restore_app_metadata` | 💾
|
|
268
|
+
| `snapshot_app_metadata` / `diff_app_metadata_snapshot` / `restore_app_metadata` / `restore_screenshots` | 💾 Back up / compare / restore an app's metadata. Text is always saved; pass `includeScreenshots:true` to also download the images so **deleted screenshots can be re-uploaded** |
|
|
269
269
|
| `release_readiness_check` | ✅ One-call **go/no-go report** — build, metadata, ASO, screenshots, compliance, TestFlight, reviews |
|
|
270
270
|
| `aso_opportunity_report` / `portfolio_growth_report` | 📈 Rank the easiest **ASO wins** across all apps; portfolio snapshot of units sold per app |
|
|
271
271
|
| `add_build_to_beta_group` / `submit_beta_review` | ✈️ Assign a build to a TestFlight group; submit for beta review |
|
package/docs/TOOLS.md
CHANGED
|
@@ -398,18 +398,27 @@ safe-mode write settings. Run this first when something isn't working.
|
|
|
398
398
|
|
|
399
399
|
### snapshot_app_metadata
|
|
400
400
|
Save a timestamped JSON snapshot of an app's editable **text** metadata (name,
|
|
401
|
-
subtitle, privacy, description, keywords, promo, what's-new, URLs, per locale)
|
|
402
|
-
|
|
403
|
-
- `
|
|
401
|
+
subtitle, privacy, description, keywords, promo, what's-new, URLs, per locale).
|
|
402
|
+
- `appId` **(required)**, `label` (optional)
|
|
403
|
+
- `includeScreenshots` — also **download the screenshot images** to a local
|
|
404
|
+
folder so deleted screenshots can be restored. Slower/larger; for apps with
|
|
405
|
+
many locales it can take a while.
|
|
406
|
+
- Returns the snapshot file path (+ `assetsDir` when images were saved).
|
|
404
407
|
|
|
405
408
|
### diff_app_metadata_snapshot
|
|
406
409
|
- `appId` **(required)**, `snapshotFile` **(required)** — current vs snapshot diff.
|
|
407
410
|
|
|
408
411
|
### restore_app_metadata
|
|
409
|
-
Restore text metadata from a snapshot (writes to the listing draft).
|
|
410
|
-
not restored. `dryRun` to preview.
|
|
412
|
+
Restore **text** metadata from a snapshot (writes to the listing draft). `dryRun` to preview.
|
|
411
413
|
- `appId` **(required)**, `snapshotFile` **(required)**, `dryRun`
|
|
412
414
|
|
|
415
|
+
### restore_screenshots
|
|
416
|
+
Re-upload screenshots from a snapshot that was taken with `includeScreenshots:true`
|
|
417
|
+
— e.g. after some were deleted. Finds/creates each set and uploads the saved images.
|
|
418
|
+
- `appId` **(required)**, `snapshotFile` **(required)**
|
|
419
|
+
- `replace` — delete the set's current screenshots first (true restore)
|
|
420
|
+
- `dryRun` — preview what would be uploaded
|
|
421
|
+
|
|
413
422
|
## Safe mode (guardrails)
|
|
414
423
|
|
|
415
424
|
Set these env vars to enforce limits at the **server** (blocked calls return a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-api-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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/guardrails.js
CHANGED
package/src/index.js
CHANGED
|
@@ -163,14 +163,19 @@ const VERSION_LOC_FIELDS = [
|
|
|
163
163
|
"supportUrl",
|
|
164
164
|
];
|
|
165
165
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Collect an app's editable metadata (snapshot/diff/restore payload).
|
|
168
|
+
* If opts.assetsDir is set, the actual screenshot IMAGES are downloaded there so
|
|
169
|
+
* deleted screenshots can be re-uploaded later.
|
|
170
|
+
*/
|
|
171
|
+
async function collectAppMetadata(appId, opts = {}) {
|
|
168
172
|
const app = await client.get(`/apps/${appId}`);
|
|
169
173
|
const snap = {
|
|
170
174
|
appId,
|
|
171
175
|
name: app.data.attributes.name,
|
|
172
176
|
bundleId: app.data.attributes.bundleId,
|
|
173
177
|
capturedAt: new Date().toISOString(),
|
|
178
|
+
screenshotsBackedUp: !!opts.assetsDir,
|
|
174
179
|
appInfo: null,
|
|
175
180
|
version: null,
|
|
176
181
|
screenshots: [],
|
|
@@ -194,16 +199,34 @@ async function collectAppMetadata(appId) {
|
|
|
194
199
|
const o = {};
|
|
195
200
|
for (const f of VERSION_LOC_FIELDS) o[f] = l.attributes[f] ?? null;
|
|
196
201
|
snap.version.localizations[l.attributes.locale] = { id: l.id, ...o };
|
|
197
|
-
// screenshot references (not the pixels)
|
|
198
202
|
const sets = await client.getAll(`/appStoreVersionLocalizations/${l.id}/appScreenshotSets`);
|
|
199
203
|
for (const s of sets) {
|
|
200
204
|
const shots = await client.getAll(`/appScreenshotSets/${s.id}/appScreenshots`);
|
|
201
|
-
if (shots.length)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
if (!shots.length) continue;
|
|
206
|
+
const items = [];
|
|
207
|
+
for (let i = 0; i < shots.length; i++) {
|
|
208
|
+
const x = shots[i];
|
|
209
|
+
const item = { id: x.id, fileName: x.attributes.fileName, order: i };
|
|
210
|
+
if (opts.assetsDir && x.attributes.imageAsset) {
|
|
211
|
+
// Download the real image so it can be re-uploaded after a deletion.
|
|
212
|
+
const dir = join(opts.assetsDir, l.attributes.locale.replace(/[^\w-]/g, "_"), s.attributes.screenshotDisplayType);
|
|
213
|
+
mkdirSync(dir, { recursive: true });
|
|
214
|
+
const url = AppStoreConnectClient.imageUrlFromAsset(x.attributes.imageAsset, 0, "png");
|
|
215
|
+
const buf = await client.fetchBinary(url);
|
|
216
|
+
const fname = `${String(i).padStart(2, "0")}-${(x.attributes.fileName || "shot").replace(/[^\w.-]/g, "_")}`;
|
|
217
|
+
const localPath = join(dir, fname.endsWith(".png") ? fname : fname + ".png");
|
|
218
|
+
writeFileSync(localPath, buf);
|
|
219
|
+
item.localPath = localPath;
|
|
220
|
+
}
|
|
221
|
+
items.push(item);
|
|
222
|
+
}
|
|
223
|
+
snap.screenshots.push({
|
|
224
|
+
locale: l.attributes.locale,
|
|
225
|
+
localizationId: l.id,
|
|
226
|
+
displayType: s.attributes.screenshotDisplayType,
|
|
227
|
+
setId: s.id,
|
|
228
|
+
items,
|
|
229
|
+
});
|
|
207
230
|
}
|
|
208
231
|
}
|
|
209
232
|
}
|
|
@@ -2152,22 +2175,29 @@ const tools = [
|
|
|
2152
2175
|
{
|
|
2153
2176
|
name: "snapshot_app_metadata",
|
|
2154
2177
|
description:
|
|
2155
|
-
"Save a timestamped JSON snapshot of an app's editable TEXT metadata (name, subtitle, privacy
|
|
2178
|
+
"Save a timestamped JSON snapshot of an app's editable TEXT metadata (name, subtitle, privacy, description, keywords, promo, what's-new, URLs — across locales). Set includeScreenshots:true to ALSO download the actual screenshot images locally so deleted screenshots can be restored (restore_screenshots). Returns the snapshot file path.",
|
|
2156
2179
|
inputSchema: {
|
|
2157
2180
|
type: "object",
|
|
2158
2181
|
properties: {
|
|
2159
2182
|
appId: { type: "string" },
|
|
2160
2183
|
label: { type: "string", description: "Optional label added to the filename" },
|
|
2184
|
+
includeScreenshots: {
|
|
2185
|
+
type: "boolean",
|
|
2186
|
+
description: "Also download the screenshot images so they can be restored (slower, larger)",
|
|
2187
|
+
},
|
|
2161
2188
|
},
|
|
2162
2189
|
required: ["appId"],
|
|
2163
2190
|
},
|
|
2164
2191
|
run: async (a) => {
|
|
2165
|
-
const snap = await collectAppMetadata(a.appId);
|
|
2166
2192
|
mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
|
2167
|
-
const
|
|
2168
|
-
const
|
|
2169
|
-
const
|
|
2193
|
+
const slug = (a.appId || "").replace(/[^\w.-]/g, "_");
|
|
2194
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2195
|
+
const base = `${slug}-${a.label ? a.label + "-" : ""}${stamp}`;
|
|
2196
|
+
const assetsDir = a.includeScreenshots ? join(SNAPSHOT_DIR, `${base}-assets`) : null;
|
|
2197
|
+
const snap = await collectAppMetadata(a.appId, { assetsDir });
|
|
2198
|
+
const file = join(SNAPSHOT_DIR, `${base}.json`);
|
|
2170
2199
|
writeFileSync(file, JSON.stringify(snap, null, 2));
|
|
2200
|
+
const shotCount = snap.screenshots.reduce((n, s) => n + s.items.length, 0);
|
|
2171
2201
|
return {
|
|
2172
2202
|
file,
|
|
2173
2203
|
app: snap.name,
|
|
@@ -2176,7 +2206,12 @@ const tools = [
|
|
|
2176
2206
|
version: snap.version ? Object.keys(snap.version.localizations).length : 0,
|
|
2177
2207
|
},
|
|
2178
2208
|
screenshotSets: snap.screenshots.length,
|
|
2179
|
-
|
|
2209
|
+
screenshots: shotCount,
|
|
2210
|
+
screenshotImagesBackedUp: !!assetsDir,
|
|
2211
|
+
assetsDir,
|
|
2212
|
+
note: a.includeScreenshots
|
|
2213
|
+
? "Text metadata AND screenshot images saved — fully restorable, including deleted screenshots."
|
|
2214
|
+
: "Text metadata saved. Screenshot IMAGES were NOT backed up — pass includeScreenshots:true to make deleted screenshots restorable.",
|
|
2180
2215
|
};
|
|
2181
2216
|
},
|
|
2182
2217
|
},
|
|
@@ -2253,6 +2288,68 @@ const tools = [
|
|
|
2253
2288
|
return { dryRun: !!a.dryRun, app: current.name, restored: actions.length, actions };
|
|
2254
2289
|
},
|
|
2255
2290
|
},
|
|
2291
|
+
{
|
|
2292
|
+
name: "restore_screenshots",
|
|
2293
|
+
description:
|
|
2294
|
+
"Re-upload screenshots from a snapshot taken with includeScreenshots:true — e.g. after some were deleted. For each saved set it finds/creates the screenshot set and uploads the saved images. Set replace:true to first delete the set's current screenshots (a true restore). dryRun to preview. WRITES screenshots — confirm with the user first.",
|
|
2295
|
+
inputSchema: {
|
|
2296
|
+
type: "object",
|
|
2297
|
+
properties: {
|
|
2298
|
+
appId: { type: "string" },
|
|
2299
|
+
snapshotFile: { type: "string" },
|
|
2300
|
+
replace: { type: "boolean", description: "Delete existing screenshots in each set before re-uploading" },
|
|
2301
|
+
dryRun: { type: "boolean" },
|
|
2302
|
+
},
|
|
2303
|
+
required: ["appId", "snapshotFile"],
|
|
2304
|
+
},
|
|
2305
|
+
run: async (a) => {
|
|
2306
|
+
if (!existsSync(a.snapshotFile)) return { error: `Snapshot not found: ${a.snapshotFile}` };
|
|
2307
|
+
const saved = JSON.parse(readFileSync(a.snapshotFile, "utf8"));
|
|
2308
|
+
if (!saved.screenshotsBackedUp)
|
|
2309
|
+
return { error: "This snapshot has no backed-up screenshot images. Re-snapshot with includeScreenshots:true." };
|
|
2310
|
+
// Map current locale -> version localization id, and existing sets by displayType.
|
|
2311
|
+
const current = await collectAppMetadata(a.appId);
|
|
2312
|
+
const locByLocale = {};
|
|
2313
|
+
if (current.version) for (const [loc, v] of Object.entries(current.version.localizations)) locByLocale[loc] = v.id;
|
|
2314
|
+
const actions = [];
|
|
2315
|
+
for (const set of saved.screenshots) {
|
|
2316
|
+
const withImages = set.items.filter((it) => it.localPath && existsSync(it.localPath));
|
|
2317
|
+
if (!withImages.length) { actions.push({ locale: set.locale, displayType: set.displayType, skipped: "no backed-up images on disk" }); continue; }
|
|
2318
|
+
const locId = locByLocale[set.locale];
|
|
2319
|
+
if (!locId) { actions.push({ locale: set.locale, displayType: set.displayType, skipped: "locale not present on current version" }); continue; }
|
|
2320
|
+
if (a.dryRun) {
|
|
2321
|
+
actions.push({ locale: set.locale, displayType: set.displayType, wouldUpload: withImages.length, replace: !!a.replace });
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
// find or create the set
|
|
2325
|
+
const existingSets = await client.getAll(`/appStoreVersionLocalizations/${locId}/appScreenshotSets`);
|
|
2326
|
+
let setId = existingSets.find((s) => s.attributes.screenshotDisplayType === set.displayType)?.id;
|
|
2327
|
+
if (!setId) {
|
|
2328
|
+
const created = await client.post(`/appScreenshotSets`, {
|
|
2329
|
+
data: { type: "appScreenshotSets", attributes: { screenshotDisplayType: set.displayType }, relationships: { appStoreVersionLocalization: { data: { type: "appStoreVersionLocalizations", id: locId } } } },
|
|
2330
|
+
});
|
|
2331
|
+
setId = created.data.id;
|
|
2332
|
+
} else if (a.replace) {
|
|
2333
|
+
const cur = await client.getAll(`/appScreenshotSets/${setId}/appScreenshots`);
|
|
2334
|
+
for (const c of cur) await client.delete(`/appScreenshots/${c.id}`);
|
|
2335
|
+
}
|
|
2336
|
+
let uploaded = 0;
|
|
2337
|
+
for (const it of withImages.sort((x, y) => (x.order ?? 0) - (y.order ?? 0))) {
|
|
2338
|
+
const buf = readFileSync(it.localPath);
|
|
2339
|
+
const reservation = await client.post(`/appScreenshots`, {
|
|
2340
|
+
data: { type: "appScreenshots", attributes: { fileName: it.fileName || basename(it.localPath), fileSize: buf.length }, relationships: { appScreenshotSet: { data: { type: "appScreenshotSets", id: setId } } } },
|
|
2341
|
+
});
|
|
2342
|
+
await client.uploadAsset(reservation.data.attributes.uploadOperations, buf);
|
|
2343
|
+
await client.patch(`/appScreenshots/${reservation.data.id}`, {
|
|
2344
|
+
data: { type: "appScreenshots", id: reservation.data.id, attributes: { uploaded: true, sourceFileChecksum: AppStoreConnectClient.md5(buf) } },
|
|
2345
|
+
});
|
|
2346
|
+
uploaded++;
|
|
2347
|
+
}
|
|
2348
|
+
actions.push({ locale: set.locale, displayType: set.displayType, uploaded, replaced: !!a.replace });
|
|
2349
|
+
}
|
|
2350
|
+
return { dryRun: !!a.dryRun, app: current.name, sets: actions.length, actions };
|
|
2351
|
+
},
|
|
2352
|
+
},
|
|
2256
2353
|
|
|
2257
2354
|
// ---- Recipes & aggregators (read-only) ----
|
|
2258
2355
|
{
|
|
@@ -2636,7 +2733,7 @@ Some things a great App Store listing needs live OUTSIDE App Store Connect and c
|
|
|
2636
2733
|
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.`;
|
|
2637
2734
|
|
|
2638
2735
|
const server = new Server(
|
|
2639
|
-
{ name: "appstore-api-mcp", version: "1.
|
|
2736
|
+
{ name: "appstore-api-mcp", version: "1.11.0" },
|
|
2640
2737
|
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },
|
|
2641
2738
|
);
|
|
2642
2739
|
|