appstore-api-mcp 1.1.3 → 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 +17 -0
- package/README.md +33 -1
- package/docs/TOOLS.md +53 -0
- package/package.json +1 -1
- package/src/client.js +26 -0
- package/src/index.js +301 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
16
|
+
## [1.2.0] - 2026-06-02
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`get_screenshot`** — fetch a live screenshot as an actual **image the agent
|
|
20
|
+
can see** (not just metadata). Downloads the App Store Connect image asset,
|
|
21
|
+
downscaled by default (`maxWidth`), and returns an `image/png` content block.
|
|
22
|
+
Lets an agent review/compare what's currently on a listing.
|
|
23
|
+
|
|
7
24
|
## [1.1.3] - 2026-06-02
|
|
8
25
|
|
|
9
26
|
### Changed
|
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)
|
|
@@ -242,16 +243,47 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
|
|
|
242
243
|
| `create_app_store_version_localization` | Add a new locale to a version |
|
|
243
244
|
| `list_screenshot_sets` / `create_screenshot_set` | Manage per-device screenshot sets |
|
|
244
245
|
| `list_screenshots` / `upload_screenshot` / `delete_screenshot` | Manage screenshots (upload handles the full reserve→upload→commit flow) |
|
|
246
|
+
| 👁️ `get_screenshot` | Fetch a live screenshot **as an image the agent can see** — review/compare what's currently on a listing |
|
|
245
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. |
|
|
246
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. |
|
|
247
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. |
|
|
248
|
-
| `
|
|
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. |
|
|
249
254
|
|
|
250
255
|
> 🛡️ **Dry-run:** the `update_*` tools accept `dryRun: true` — they return a field-by-field
|
|
251
256
|
> diff (old → new) with length/limit checks and **write nothing**. Drop the flag to apply.
|
|
252
257
|
|
|
253
258
|
---
|
|
254
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
|
+
|
|
255
287
|
## Common workflows
|
|
256
288
|
|
|
257
289
|
### Update keywords or description
|
package/docs/TOOLS.md
CHANGED
|
@@ -117,6 +117,13 @@ Common `displayType` values: `APP_IPHONE_67`, `APP_IPHONE_65`, `APP_IPHONE_61`,
|
|
|
117
117
|
### `list_screenshots`
|
|
118
118
|
- `screenshotSetId` **(required)**
|
|
119
119
|
|
|
120
|
+
### `get_screenshot`
|
|
121
|
+
Fetch the actual screenshot **image** (not just metadata) and return it as an
|
|
122
|
+
image the agent can see — review/compare what's currently live.
|
|
123
|
+
- `screenshotId` **(required)**
|
|
124
|
+
- `maxWidth` — downscale width in px for a lighter preview (default `750`; `0` = full size)
|
|
125
|
+
- **Returns:** an `image/png` content block + a caption with the file name and original dimensions.
|
|
126
|
+
|
|
120
127
|
### `upload_screenshot`
|
|
121
128
|
Upload an image into a set. Handles reserve → upload bytes → commit (with checksum).
|
|
122
129
|
- `screenshotSetId` **(required)**
|
|
@@ -259,6 +266,52 @@ Download + decompress + parse an instance's segments into rows.
|
|
|
259
266
|
|
|
260
267
|
---
|
|
261
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
|
+
|
|
262
315
|
## Raw API access
|
|
263
316
|
|
|
264
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/client.js
CHANGED
|
@@ -193,6 +193,32 @@ export class AppStoreConnectClient {
|
|
|
193
193
|
: buf.toString("utf8");
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/** Fetch raw bytes from a URL (e.g. an image asset). Returns a Buffer. */
|
|
197
|
+
async fetchBinary(url) {
|
|
198
|
+
const res = await fetch(url);
|
|
199
|
+
if (!res.ok)
|
|
200
|
+
throw new Error(`Image fetch failed (${res.status}) for ${url}`);
|
|
201
|
+
return Buffer.from(await res.arrayBuffer());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build a concrete image URL from an App Store Connect imageAsset
|
|
206
|
+
* (`{ templateUrl, width, height }`), optionally downscaled to maxWidth.
|
|
207
|
+
*/
|
|
208
|
+
static imageUrlFromAsset(imageAsset, maxWidth, format = "png") {
|
|
209
|
+
if (!imageAsset || !imageAsset.templateUrl) return null;
|
|
210
|
+
let w = imageAsset.width || 0;
|
|
211
|
+
let h = imageAsset.height || 0;
|
|
212
|
+
if (maxWidth && w && h && w > maxWidth) {
|
|
213
|
+
h = Math.round((h * maxWidth) / w);
|
|
214
|
+
w = maxWidth;
|
|
215
|
+
}
|
|
216
|
+
return imageAsset.templateUrl
|
|
217
|
+
.replace("{w}", String(w))
|
|
218
|
+
.replace("{h}", String(h))
|
|
219
|
+
.replace("{f}", format);
|
|
220
|
+
}
|
|
221
|
+
|
|
196
222
|
/** Parse TSV/CSV text into an array of row objects. Auto-detects delimiter. */
|
|
197
223
|
static parseDelimited(text, delimiter) {
|
|
198
224
|
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
package/src/index.js
CHANGED
|
@@ -619,6 +619,49 @@ const tools = [
|
|
|
619
619
|
return committed;
|
|
620
620
|
},
|
|
621
621
|
},
|
|
622
|
+
{
|
|
623
|
+
name: "get_screenshot",
|
|
624
|
+
description:
|
|
625
|
+
"Fetch the actual screenshot IMAGE by its id and return it so the agent can SEE it (not just metadata). Downloads the live image asset from App Store Connect, downscaled for a quick preview by default. Use this to review/compare what's currently live on a listing.",
|
|
626
|
+
inputSchema: {
|
|
627
|
+
type: "object",
|
|
628
|
+
properties: {
|
|
629
|
+
screenshotId: { type: "string" },
|
|
630
|
+
maxWidth: {
|
|
631
|
+
type: "number",
|
|
632
|
+
description: "Downscale to this width in px for a lighter preview (default 750; pass 0 for full size)",
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
required: ["screenshotId"],
|
|
636
|
+
},
|
|
637
|
+
run: async (a) => {
|
|
638
|
+
const res = await client.get(`/appScreenshots/${a.screenshotId}`);
|
|
639
|
+
const attr = res.data.attributes || {};
|
|
640
|
+
const asset = attr.imageAsset;
|
|
641
|
+
if (!asset || !asset.templateUrl)
|
|
642
|
+
return {
|
|
643
|
+
note: "This screenshot has no rendered image yet (still uploading/processing). State: " +
|
|
644
|
+
(attr.assetDeliveryState && attr.assetDeliveryState.state),
|
|
645
|
+
fileName: attr.fileName,
|
|
646
|
+
};
|
|
647
|
+
const maxWidth = a.maxWidth === undefined ? 750 : a.maxWidth || 0;
|
|
648
|
+
const url = AppStoreConnectClient.imageUrlFromAsset(asset, maxWidth, "png");
|
|
649
|
+
const buf = await client.fetchBinary(url);
|
|
650
|
+
return {
|
|
651
|
+
__mcpContent: [
|
|
652
|
+
{
|
|
653
|
+
type: "image",
|
|
654
|
+
data: buf.toString("base64"),
|
|
655
|
+
mimeType: "image/png",
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
type: "text",
|
|
659
|
+
text: `Screenshot ${a.screenshotId} — ${attr.fileName || "(no name)"}, original ${asset.width}×${asset.height}.`,
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
},
|
|
622
665
|
{
|
|
623
666
|
name: "delete_screenshot",
|
|
624
667
|
description: "Delete a screenshot by its id.",
|
|
@@ -1038,6 +1081,262 @@ const tools = [
|
|
|
1038
1081
|
},
|
|
1039
1082
|
},
|
|
1040
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
|
+
|
|
1041
1340
|
// ---- Generic escape hatch ----
|
|
1042
1341
|
{
|
|
1043
1342
|
name: "raw_request",
|
|
@@ -1091,6 +1390,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1091
1390
|
if (!tool) return fail(new Error(`Unknown tool: ${req.params.name}`));
|
|
1092
1391
|
try {
|
|
1093
1392
|
const result = await tool.run(req.params.arguments || {});
|
|
1393
|
+
// Tools may return raw MCP content (e.g. images) via __mcpContent.
|
|
1394
|
+
if (result && result.__mcpContent) return { content: result.__mcpContent };
|
|
1094
1395
|
return ok(result);
|
|
1095
1396
|
} catch (e) {
|
|
1096
1397
|
if (e && e.status === 403 && REPORT_TOOLS.has(req.params.name))
|