appstore-api-mcp 1.0.4 → 1.1.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/.env.example CHANGED
@@ -18,3 +18,9 @@ ASC_PRIVATE_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
18
18
 
19
19
  # 3) ...or base64 of the .p8 file (no newline headaches): base64 -i AuthKey_XXXX.p8
20
20
  # ASC_PRIVATE_KEY_BASE64=
21
+
22
+ # Optional: default Vendor Number for sales/finance reports (8–9 digits).
23
+ # Found in App Store Connect → Payments and Financial Reports (or Sales and Trends).
24
+ # Sales/finance/analytics reports also require a key with the Admin, Finance, or
25
+ # Sales role — an App Manager key cannot read them.
26
+ # ASC_VENDOR_NUMBER=
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.1.0] - 2026-06-02
8
+
9
+ ### Added
10
+ - **Analytics, sales, subscriptions & finance reporting:**
11
+ - `get_sales_report` — units/downloads, proceeds, and subscription data (Sales & Trends).
12
+ - `get_subscription_report` — active subscribers, events, and per-subscriber detail.
13
+ - `get_finance_report` — proceeds/earnings by region.
14
+ - `request_analytics_report` + `list_analytics_reports` +
15
+ `list_analytics_report_instances` + `get_analytics_report_data` — the async
16
+ Analytics Reports API (downloads, sessions, active devices, engagement).
17
+ - Gzip/TSV/CSV handling in the client so report files are decompressed and
18
+ returned as parsed rows.
19
+ - Optional `ASC_VENDOR_NUMBER` env var as a default for sales/finance reports.
20
+ - Friendly 403 hint: report APIs need a key with the Admin, Finance, or Sales
21
+ role (App Manager is not sufficient).
22
+
7
23
  ## [1.0.4] - 2026-06-02
8
24
 
9
25
  ### Changed
package/README.md CHANGED
@@ -1,22 +1,44 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fil-technology/appstore-api-mcp/main/assets/banner.png" alt="appstore-api-mcp — manage App Store Connect from any AI agent" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/appstore-api-mcp"><img src="https://img.shields.io/npm/v/appstore-api-mcp?color=0a84ff&label=npm" alt="npm version"></a>
7
+ <a href="https://www.npmjs.com/package/appstore-api-mcp"><img src="https://img.shields.io/npm/dm/appstore-api-mcp?color=22d3ee" alt="npm downloads"></a>
8
+ <img src="https://img.shields.io/npm/l/appstore-api-mcp?color=blue" alt="license">
9
+ <img src="https://img.shields.io/node/v/appstore-api-mcp" alt="node version">
10
+ </p>
11
+
1
12
  # App Store Connect MCP
2
13
 
3
- An [MCP](https://modelcontextprotocol.io) server that lets **any AI agent** read
4
- and edit your **App Store Connect** apps in plain language — keywords,
5
- descriptions, titles, subtitles, promotional text, what's-new, screenshots,
6
- versions — plus a raw-request tool that reaches the **entire**
7
- [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi).
14
+ **Run App Store Connect by talking to your AI.** An
15
+ [MCP](https://modelcontextprotocol.io) server that turns Apple's
16
+ [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi)
17
+ into plain-language actions — edit your listings, audit your whole portfolio,
18
+ and reach the entire API, from whatever AI agent you already use.
19
+
20
+ ### ✨ What makes it stand out
21
+
22
+ > **🚀 Audit your whole portfolio in one shot.** *"Audit all my apps for ASO gaps"* → a ranked report across **every** app: empty keyword fields, missing subtitles, under-used 100‑char keyword space, single‑locale listings, missing screenshots. Built for indies shipping dozens of apps — not one at a time.
23
+
24
+ > **🛡️ See every change before it ships.** Dry-run mode returns the exact diff (old → new) with Apple's character limits pre-checked, so nothing hits your **live** listing by surprise.
25
+
26
+ > **📊 Track performance, not just listings.** Pull downloads, proceeds, **subscriptions & retention**, and engagement straight from the Sales, Finance, and Analytics report APIs — parsed into rows, gzip handled for you.
8
27
 
9
- > Ask things like *"update the keywords for my app to X, Y, Z"*,
10
- > *"show the English description for MyApp"*, or
11
- > *"upload these screenshots to the 6.7-inch set"* — the agent calls the right tools.
28
+ > **🌐 The whole API, not a curated slice.** First-class tools for the daily work (keywords, descriptions, titles, subtitles, screenshots, versions) **plus** a `raw_request` escape hatch for everything else — TestFlight, pricing, in-app purchases, customer reviews (read & reply), and more.
12
29
 
13
- - 🤖 **Works with any MCP client** — Claude Code/Desktop, OpenAI Codex CLI, Cursor, Cline, Windsurf, VS Code (agent mode), Zed, Continue, Gemini CLI, Google Antigravity, Amazon Q, Goose, JetBrains AI, Warp, and custom agents on the MCP SDKs. Standard stdio server, no client-specific code. → [docs/CLIENTS.md](docs/CLIENTS.md)
14
- - ✅ **One-line install** via `npx` — no clone, no build
15
- - ✅ **Credentials stay on your machine** — calls go straight to Apple, nothing is proxied
16
- - ✅ **Slim, well-described tool set** + a `raw_request` escape hatch for the whole API
17
- - 🛡️ **Dry-run mode** — preview any metadata change (old→new + length checks) before writing
18
- - 🚀 **Fleet audit** — one call health-checks **all** your apps for ASO gaps (built for indies with many apps)
19
- - ✅ MIT licensed, actively maintained
30
+ ### 💬 Just ask
31
+
32
+ > *“Set the keywords for my budgeting app to budget, expenses, money tracker.”*
33
+ > *“Which of my apps are missing a subtitle or screenshots?”*
34
+ > *“Dry-run a shorter description for MyApp, then upload these 6.7″ screenshots.”*
35
+
36
+ ### ⚙️ Built right
37
+
38
+ - 🤖 **Any MCP client** — Claude, Codex, Cursor, Windsurf, Antigravity, Gemini CLI, Amazon Q, Goose, Zed, VS Code… → [docs/CLIENTS.md](docs/CLIENTS.md)
39
+ - ⚡ **One-line install** (`npx`, no build) — or paste a prompt and let your agent set it up for you
40
+ - 🔒 **Keys never leave your machine** — calls go straight to Apple, nothing proxied
41
+ - 📦 **MIT · published with provenance · actively maintained**
20
42
 
21
43
  ---
22
44
 
@@ -162,6 +184,14 @@ Copy-paste config snippets for each are in **[docs/CLIENTS.md](docs/CLIENTS.md)*
162
184
  3. Note the **Issuer ID** (top of the page) and the **Key ID** (next to the key).
163
185
  4. **Download the `.p8` file** — you can only download it once. Store it somewhere safe.
164
186
 
187
+ <p align="center">
188
+ <img src="https://raw.githubusercontent.com/fil-technology/appstore-api-mcp/main/assets/where-to-find-credentials.png" alt="Where to find the Issuer ID and Key ID in App Store Connect → Users and Access → Integrations → App Store Connect API" width="100%">
189
+ </p>
190
+
191
+ > The **Issuer ID** is at the top of the page; each key's **Key ID** is in its
192
+ > row. The `.p8` is downloaded from the **+** / key actions. These map to
193
+ > `ASC_ISSUER_ID`, `ASC_KEY_ID`, and `ASC_PRIVATE_KEY_PATH`.
194
+
165
195
  Full walkthrough with screenshots-worth of detail: **[docs/SETUP.md](docs/SETUP.md)**.
166
196
 
167
197
  ---
@@ -177,9 +207,15 @@ All configuration is via environment variables.
177
207
  | `ASC_PRIVATE_KEY_PATH` | one of these three | Absolute path to the `.p8` file |
178
208
  | `ASC_PRIVATE_KEY` | one of these three | The raw PEM contents of the key |
179
209
  | `ASC_PRIVATE_KEY_BASE64` | one of these three | Base64 of the `.p8` (`base64 -i AuthKey.p8`) — easiest for env vars |
210
+ | `ASC_VENDOR_NUMBER` | optional | Default Vendor Number for sales/finance reports (8–9 digits; App Store Connect → Payments and Financial Reports) |
180
211
 
181
212
  See [.env.example](.env.example) for a copy-paste template.
182
213
 
214
+ > **Reports need a higher-privilege key.** The sales/subscription/finance/analytics
215
+ > tools require an API key with the **Admin, Finance, or Sales** role — an **App
216
+ > Manager** key (fine for metadata/keywords/screenshots) returns `403` for reports.
217
+ > Generate a separate report-capable key if you need analytics.
218
+
183
219
  ---
184
220
 
185
221
  ## Tools
@@ -203,7 +239,9 @@ Full parameter reference: **[docs/TOOLS.md](docs/TOOLS.md)**.
203
239
  | `list_screenshot_sets` / `create_screenshot_set` | Manage per-device screenshot sets |
204
240
  | `list_screenshots` / `upload_screenshot` / `delete_screenshot` | Manage screenshots (upload handles the full reserve→upload→commit flow) |
205
241
  | 🚀 `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. |
206
- | `raw_request` | Any method/path against the API — previews, pricing, TestFlight, IAP, reviews, analytics, sales reports, … |
242
+ | 📊 `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. |
243
+ | 📈 `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. |
244
+ | `raw_request` | Any method/path against the API — previews, pricing, TestFlight, IAP, reviews, … |
207
245
 
208
246
  > 🛡️ **Dry-run:** the `update_*` tools accept `dryRun: true` — they return a field-by-field
209
247
  > diff (old → new) with length/limit checks and **write nothing**. Drop the flag to apply.
package/docs/SETUP.md CHANGED
@@ -11,6 +11,8 @@ Step-by-step from zero to a working server.
11
11
 
12
12
  ## 2. Create an App Store Connect API key
13
13
 
14
+ ![Where to find the Issuer ID and Key ID in App Store Connect](https://raw.githubusercontent.com/fil-technology/appstore-api-mcp/main/assets/where-to-find-credentials.png)
15
+
14
16
  1. Sign in to [App Store Connect](https://appstoreconnect.apple.com).
15
17
  2. Go to **Users and Access** → **Integrations** tab → **App Store Connect API**.
16
18
  3. Copy the **Issuer ID** shown near the top — this is your `ASC_ISSUER_ID`
package/docs/TOOLS.md CHANGED
@@ -200,6 +200,64 @@ length warnings are attached to the response under `_warnings`.
200
200
 
201
201
  ---
202
202
 
203
+ ## Analytics, sales, subscriptions & finance
204
+
205
+ > **Permissions:** these tools require an API key with the **Admin, Finance, or
206
+ > Sales** role. An **App Manager** key returns `403`. Sales/finance also need a
207
+ > **Vendor Number** (App Store Connect → Payments and Financial Reports; 8–9
208
+ > digits) — pass `vendorNumber` or set the `ASC_VENDOR_NUMBER` env var.
209
+
210
+ All report tools return `{ reportType, columns, rowCount, returned, truncated, rows }`
211
+ with `rows` capped at `limit` (default 200).
212
+
213
+ ### get_sales_report
214
+ Units/downloads, proceeds, and subscription data from Sales & Trends.
215
+ - `reportDate` **(required)** — `YYYY-MM-DD` (daily/weekly), `YYYY-MM` (monthly), `YYYY` (yearly)
216
+ - `vendorNumber` — or `ASC_VENDOR_NUMBER`
217
+ - `frequency` — `DAILY` (default), `WEEKLY`, `MONTHLY`, `YEARLY`
218
+ - `reportType` — `SALES` (default), `SUBSCRIPTION`, `SUBSCRIBER`, `SUBSCRIPTION_EVENT`, `INSTALLS`, …
219
+ - `reportSubType` — `SUMMARY` (default) or `DETAILED`
220
+ - `version` — report-version override (e.g. `1_1` for SALES, `1_4` for subscriptions)
221
+ - `limit` — max rows (default 200)
222
+
223
+ ### get_subscription_report
224
+ Convenience wrapper for subscription analytics (DAILY only).
225
+ - `reportDate` **(required)** — `YYYY-MM-DD`
226
+ - `kind` — `ACTIVE` (default, active-subscriber snapshot), `EVENTS` (subscribe/cancel/renew/retention), `SUBSCRIBERS` (per-subscriber detail)
227
+ - `vendorNumber`, `limit`
228
+
229
+ ### get_finance_report
230
+ Proceeds/earnings by region.
231
+ - `reportDate` **(required)** — fiscal month `YYYY-MM`
232
+ - `regionCode` — `ZZ` (default, consolidated), `US`, `EU`, `JP`, …
233
+ - `vendorNumber`, `limit`
234
+
235
+ ### request_analytics_report
236
+ Start an Analytics report request for an app (downloads, sessions, active
237
+ devices, App Store engagement). **Async** — generation can take minutes to hours.
238
+ - `appId` **(required)**
239
+ - `accessType` — `ONE_TIME_SNAPSHOT` (default) or `ONGOING`
240
+
241
+ ### list_analytics_reports
242
+ - `requestId` **(required)** — from `request_analytics_report`
243
+ - `category` — `APP_USAGE`, `APP_STORE_ENGAGEMENT`, `COMMERCE`, `FRAMEWORK_USAGE`, `PERFORMANCE`
244
+
245
+ ### list_analytics_report_instances
246
+ - `reportId` **(required)**
247
+ - `granularity` — `DAILY`, `WEEKLY`, `MONTHLY`
248
+ - `processingDate` — `YYYY-MM-DD`
249
+
250
+ ### get_analytics_report_data
251
+ Download + decompress + parse an instance's segments into rows.
252
+ - `instanceId` **(required)**
253
+ - `limit` — max rows (default 200)
254
+
255
+ **Typical analytics flow:** `request_analytics_report` → (wait) →
256
+ `list_analytics_reports` → `list_analytics_report_instances` →
257
+ `get_analytics_report_data`.
258
+
259
+ ---
260
+
203
261
  ## Raw API access
204
262
 
205
263
  ### `raw_request`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-api-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Apple App Store Connect — manage apps, keywords, descriptions, titles, screenshots, versions and the full API from any MCP client (Claude, Codex, Cursor, Cline, Windsurf, VS Code, Zed, Continue, Gemini CLI, Google Antigravity, Amazon Q, Goose, and custom agents). Includes a fleet-wide ASO audit and dry-run previews.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
+ import { gunzipSync } from "node:zlib";
3
4
  import { importPKCS8, SignJWT } from "jose";
4
5
 
5
6
  const BASE_URL = "https://api.appstoreconnect.apple.com";
@@ -54,8 +55,7 @@ export class AppStoreConnectClient {
54
55
  * Core request. `path` may be a full URL (e.g. a paging `next` link) or a
55
56
  * path relative to the API root, with or without the leading /v1.
56
57
  */
57
- async request(method, path, { query, body } = {}) {
58
- const token = await this._getToken();
58
+ _buildUrl(path, query) {
59
59
  let url;
60
60
  if (/^https?:\/\//.test(path)) {
61
61
  url = new URL(path);
@@ -72,6 +72,12 @@ export class AppStoreConnectClient {
72
72
  url.searchParams.set(k, Array.isArray(v) ? v.join(",") : String(v));
73
73
  }
74
74
  }
75
+ return url;
76
+ }
77
+
78
+ async request(method, path, { query, body } = {}) {
79
+ const token = await this._getToken();
80
+ const url = this._buildUrl(path, query);
75
81
  const headers = { Authorization: `Bearer ${token}` };
76
82
  let payload;
77
83
  if (body !== undefined) {
@@ -133,6 +139,72 @@ export class AppStoreConnectClient {
133
139
  return all;
134
140
  }
135
141
 
142
+ /** True if the buffer starts with the gzip magic bytes. */
143
+ static _isGzip(buf) {
144
+ return buf.length > 2 && buf[0] === 0x1f && buf[1] === 0x8b;
145
+ }
146
+
147
+ /**
148
+ * GET a report endpoint (salesReports / financeReports) that returns a
149
+ * gzip-compressed TSV. Returns the decompressed text. On error, the API
150
+ * sends JSON instead of gzip — surfaced as a readable Error.
151
+ */
152
+ async getReport(path, query) {
153
+ const token = await this._getToken();
154
+ const url = this._buildUrl(path, query);
155
+ const res = await fetch(url, {
156
+ method: "GET",
157
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/a-gzip" },
158
+ });
159
+ const buf = Buffer.from(await res.arrayBuffer());
160
+ if (!res.ok) {
161
+ let detail = buf.toString("utf8");
162
+ try {
163
+ const j = JSON.parse(detail);
164
+ if (j.errors)
165
+ detail = j.errors
166
+ .map((e) => `${e.status} ${e.code}: ${e.title} — ${e.detail}`)
167
+ .join("; ");
168
+ } catch {
169
+ /* leave detail as text */
170
+ }
171
+ const err = new Error(
172
+ `App Store Connect report ${url.pathname} failed: ${res.status} ${res.statusText} — ${detail}`,
173
+ );
174
+ err.status = res.status;
175
+ throw err;
176
+ }
177
+ return AppStoreConnectClient._isGzip(buf)
178
+ ? gunzipSync(buf).toString("utf8")
179
+ : buf.toString("utf8");
180
+ }
181
+
182
+ /** Download a (possibly gzipped) report/segment file from a pre-signed URL. */
183
+ async downloadUrl(url) {
184
+ const res = await fetch(url);
185
+ const buf = Buffer.from(await res.arrayBuffer());
186
+ if (!res.ok)
187
+ throw new Error(`Download failed (${res.status}) for ${url}`);
188
+ return AppStoreConnectClient._isGzip(buf)
189
+ ? gunzipSync(buf).toString("utf8")
190
+ : buf.toString("utf8");
191
+ }
192
+
193
+ /** Parse TSV/CSV text into an array of row objects. Auto-detects delimiter. */
194
+ static parseDelimited(text, delimiter) {
195
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
196
+ if (lines.length === 0) return { columns: [], rows: [] };
197
+ const d = delimiter || (lines[0].includes("\t") ? "\t" : ",");
198
+ const columns = lines[0].split(d);
199
+ const rows = lines.slice(1).map((line) => {
200
+ const cells = line.split(d);
201
+ const row = {};
202
+ columns.forEach((c, i) => (row[c] = cells[i]));
203
+ return row;
204
+ });
205
+ return { columns, rows };
206
+ }
207
+
136
208
  /**
137
209
  * Upload an image to a freshly-reserved upload-asset (screenshot or preview).
138
210
  * `uploadOperations` come from the reservation's attributes.
package/src/index.js CHANGED
@@ -128,6 +128,45 @@ const EDITABLE_VERSION_STATES = new Set([
128
128
  "INVALID_BINARY",
129
129
  ]);
130
130
 
131
+ // Optional default Vendor Number for sales/finance reports.
132
+ const DEFAULT_VENDOR = process.env.ASC_VENDOR_NUMBER;
133
+
134
+ /** Cap parsed report rows so large reports don't flood the response. */
135
+ function reportResult(reportType, parsed, limit = 200) {
136
+ const rows = parsed.rows;
137
+ return {
138
+ reportType,
139
+ columns: parsed.columns,
140
+ rowCount: rows.length,
141
+ returned: Math.min(rows.length, limit),
142
+ truncated: rows.length > limit,
143
+ rows: rows.slice(0, limit),
144
+ };
145
+ }
146
+
147
+ function requireVendor(v) {
148
+ const vendor = v || DEFAULT_VENDOR;
149
+ if (!vendor)
150
+ throw new Error(
151
+ "vendorNumber is required (or set the ASC_VENDOR_NUMBER env var). Find it in App Store Connect → Payments and Financial Reports (or Sales and Trends) — an 8–9 digit number.",
152
+ );
153
+ return vendor;
154
+ }
155
+
156
+ // Tools that hit role-gated report/analytics endpoints. On a 403 the dispatcher
157
+ // appends a hint that these need a higher-privilege key than App Manager.
158
+ const REPORT_TOOLS = new Set([
159
+ "get_sales_report",
160
+ "get_subscription_report",
161
+ "get_finance_report",
162
+ "request_analytics_report",
163
+ "list_analytics_reports",
164
+ "list_analytics_report_instances",
165
+ "get_analytics_report_data",
166
+ ]);
167
+ const ROLE_HINT =
168
+ " — NOTE: report/analytics APIs require an API key with the Admin, Finance, or Sales role. An App Manager key is not sufficient; generate a key with the needed role in App Store Connect → Users and Access → Integrations.";
169
+
131
170
  // ---- Tool definitions -------------------------------------------------------
132
171
 
133
172
  const tools = [
@@ -751,6 +790,250 @@ const tools = [
751
790
  },
752
791
  },
753
792
 
793
+ // ---- Analytics, sales, subscriptions & finance ----
794
+ {
795
+ name: "get_sales_report",
796
+ description:
797
+ "Download a Sales & Trends report (units/downloads, proceeds, and subscription data) and return parsed rows. Requires your Vendor Number (App Store Connect → Payments and Financial Reports / Sales and Trends; 8–9 digits) via vendorNumber or the ASC_VENDOR_NUMBER env var. reportType: SALES (units & proceeds, default), SUBSCRIPTION (active subs snapshot), SUBSCRIBER (per-subscriber detail), SUBSCRIPTION_EVENT (subscribe/cancel/renew), INSTALLS, FIRST_ANNUAL. reportDate format by frequency: DAILY/WEEKLY = YYYY-MM-DD, MONTHLY = YYYY-MM, YEARLY = YYYY.",
798
+ inputSchema: {
799
+ type: "object",
800
+ properties: {
801
+ vendorNumber: { type: "string" },
802
+ reportDate: {
803
+ type: "string",
804
+ description: "e.g. 2024-01-15 (daily) or 2024-01 (monthly)",
805
+ },
806
+ frequency: {
807
+ type: "string",
808
+ description: "DAILY (default), WEEKLY, MONTHLY, YEARLY",
809
+ },
810
+ reportType: {
811
+ type: "string",
812
+ description: "SALES (default), SUBSCRIPTION, SUBSCRIBER, SUBSCRIPTION_EVENT, INSTALLS, …",
813
+ },
814
+ reportSubType: {
815
+ type: "string",
816
+ description: "SUMMARY (default) or DETAILED",
817
+ },
818
+ version: {
819
+ type: "string",
820
+ description: "Report version override (e.g. 1_1 for SALES, 1_4 for subscriptions)",
821
+ },
822
+ limit: { type: "number", description: "Max rows to return (default 200)" },
823
+ },
824
+ required: ["reportDate"],
825
+ },
826
+ run: async (a) => {
827
+ const vendor = requireVendor(a.vendorNumber);
828
+ const reportType = a.reportType || "SALES";
829
+ const subType =
830
+ a.reportSubType || (reportType === "SUBSCRIBER" ? "DETAILED" : "SUMMARY");
831
+ const version =
832
+ a.version ||
833
+ (reportType === "SALES"
834
+ ? "1_1"
835
+ : reportType.startsWith("SUBSC")
836
+ ? "1_4"
837
+ : "1_0");
838
+ const text = await client.getReport("/salesReports", {
839
+ "filter[vendorNumber]": vendor,
840
+ "filter[frequency]": a.frequency || "DAILY",
841
+ "filter[reportType]": reportType,
842
+ "filter[reportSubType]": subType,
843
+ "filter[reportDate]": a.reportDate,
844
+ "filter[version]": version,
845
+ });
846
+ return reportResult(
847
+ reportType,
848
+ AppStoreConnectClient.parseDelimited(text, "\t"),
849
+ a.limit ?? 200,
850
+ );
851
+ },
852
+ },
853
+ {
854
+ name: "get_subscription_report",
855
+ description:
856
+ "Subscription analytics via Sales & Trends (convenience wrapper). kind: ACTIVE = current active-subscriber snapshot, EVENTS = subscribe/cancel/renew/retention events, SUBSCRIBERS = per-subscriber detail. Requires the Vendor Number. These reports are DAILY only.",
857
+ inputSchema: {
858
+ type: "object",
859
+ properties: {
860
+ vendorNumber: { type: "string" },
861
+ kind: {
862
+ type: "string",
863
+ description: "ACTIVE (default), EVENTS, SUBSCRIBERS",
864
+ },
865
+ reportDate: { type: "string", description: "YYYY-MM-DD" },
866
+ limit: { type: "number", description: "Max rows (default 200)" },
867
+ },
868
+ required: ["reportDate"],
869
+ },
870
+ run: async (a) => {
871
+ const vendor = requireVendor(a.vendorNumber);
872
+ const map = {
873
+ ACTIVE: ["SUBSCRIPTION", "SUMMARY"],
874
+ EVENTS: ["SUBSCRIPTION_EVENT", "SUMMARY"],
875
+ SUBSCRIBERS: ["SUBSCRIBER", "DETAILED"],
876
+ };
877
+ const [reportType, subType] = map[a.kind || "ACTIVE"] || map.ACTIVE;
878
+ const text = await client.getReport("/salesReports", {
879
+ "filter[vendorNumber]": vendor,
880
+ "filter[frequency]": "DAILY",
881
+ "filter[reportType]": reportType,
882
+ "filter[reportSubType]": subType,
883
+ "filter[reportDate]": a.reportDate,
884
+ "filter[version]": "1_4",
885
+ });
886
+ return reportResult(
887
+ reportType,
888
+ AppStoreConnectClient.parseDelimited(text, "\t"),
889
+ a.limit ?? 200,
890
+ );
891
+ },
892
+ },
893
+ {
894
+ name: "get_finance_report",
895
+ description:
896
+ "Download a Finance report (proceeds/earnings by region) and return parsed rows. Requires the Vendor Number, a regionCode (e.g. 'ZZ' for the consolidated/all-regions report, or 'US', 'EU', 'JP', …) and reportDate as YYYY-MM (a fiscal month).",
897
+ inputSchema: {
898
+ type: "object",
899
+ properties: {
900
+ vendorNumber: { type: "string" },
901
+ regionCode: { type: "string", description: "e.g. ZZ (default), US, EU, JP" },
902
+ reportDate: { type: "string", description: "Fiscal month, YYYY-MM" },
903
+ limit: { type: "number", description: "Max rows (default 200)" },
904
+ },
905
+ required: ["reportDate"],
906
+ },
907
+ run: async (a) => {
908
+ const vendor = requireVendor(a.vendorNumber);
909
+ const text = await client.getReport("/financeReports", {
910
+ "filter[vendorNumber]": vendor,
911
+ "filter[regionCode]": a.regionCode || "ZZ",
912
+ "filter[reportDate]": a.reportDate,
913
+ "filter[reportType]": "FINANCIAL",
914
+ });
915
+ return reportResult(
916
+ "FINANCIAL",
917
+ AppStoreConnectClient.parseDelimited(text, "\t"),
918
+ a.limit ?? 200,
919
+ );
920
+ },
921
+ },
922
+ {
923
+ name: "request_analytics_report",
924
+ description:
925
+ "Start an Analytics report request for an app — covers downloads, installs, sessions, active devices, App Store engagement (impressions, product page views, conversion), and more. accessType: ONE_TIME_SNAPSHOT (historical, default) or ONGOING (kept up to date daily). Generation is ASYNC and can take minutes to hours. Afterwards: list_analytics_reports → list_analytics_report_instances → get_analytics_report_data.",
926
+ inputSchema: {
927
+ type: "object",
928
+ properties: {
929
+ appId: { type: "string" },
930
+ accessType: {
931
+ type: "string",
932
+ description: "ONE_TIME_SNAPSHOT (default) or ONGOING",
933
+ },
934
+ },
935
+ required: ["appId"],
936
+ },
937
+ run: async (a) =>
938
+ client.post("/analyticsReportRequests", {
939
+ data: {
940
+ type: "analyticsReportRequests",
941
+ attributes: { accessType: a.accessType || "ONE_TIME_SNAPSHOT" },
942
+ relationships: { app: { data: { type: "apps", id: a.appId } } },
943
+ },
944
+ }),
945
+ },
946
+ {
947
+ name: "list_analytics_reports",
948
+ description:
949
+ "List the reports available under an analytics report request (from request_analytics_report). category filter: APP_USAGE, APP_STORE_ENGAGEMENT, COMMERCE, FRAMEWORK_USAGE, PERFORMANCE.",
950
+ inputSchema: {
951
+ type: "object",
952
+ properties: {
953
+ requestId: { type: "string" },
954
+ category: { type: "string" },
955
+ },
956
+ required: ["requestId"],
957
+ },
958
+ run: async (a) => {
959
+ const q = {};
960
+ if (a.category) q["filter[category]"] = a.category;
961
+ const data = await client.getAll(
962
+ `/analyticsReportRequests/${a.requestId}/reports`,
963
+ q,
964
+ );
965
+ return data.map((x) => ({ id: x.id, ...x.attributes }));
966
+ },
967
+ },
968
+ {
969
+ name: "list_analytics_report_instances",
970
+ description:
971
+ "List instances of an analytics report (one per processing date). granularity: DAILY, WEEKLY, MONTHLY. Use the instance id with get_analytics_report_data.",
972
+ inputSchema: {
973
+ type: "object",
974
+ properties: {
975
+ reportId: { type: "string" },
976
+ granularity: { type: "string", description: "DAILY, WEEKLY, MONTHLY" },
977
+ processingDate: { type: "string", description: "YYYY-MM-DD" },
978
+ },
979
+ required: ["reportId"],
980
+ },
981
+ run: async (a) => {
982
+ const q = {};
983
+ if (a.granularity) q["filter[granularity]"] = a.granularity;
984
+ if (a.processingDate) q["filter[processingDate]"] = a.processingDate;
985
+ const data = await client.getAll(
986
+ `/analyticsReports/${a.reportId}/instances`,
987
+ q,
988
+ );
989
+ return data.map((x) => ({ id: x.id, ...x.attributes }));
990
+ },
991
+ },
992
+ {
993
+ name: "get_analytics_report_data",
994
+ description:
995
+ "Download and parse the data for an analytics report instance. Fetches its segments (gzipped CSV), decompresses, and returns parsed rows.",
996
+ inputSchema: {
997
+ type: "object",
998
+ properties: {
999
+ instanceId: { type: "string" },
1000
+ limit: { type: "number", description: "Max rows (default 200)" },
1001
+ },
1002
+ required: ["instanceId"],
1003
+ },
1004
+ run: async (a) => {
1005
+ const segs = await client.getAll(
1006
+ `/analyticsReportInstances/${a.instanceId}/segments`,
1007
+ );
1008
+ if (!segs.length)
1009
+ return {
1010
+ rowCount: 0,
1011
+ rows: [],
1012
+ note: "No segments yet — the instance may still be processing. Try again later.",
1013
+ };
1014
+ let columns = [];
1015
+ const allRows = [];
1016
+ for (const s of segs) {
1017
+ const url = s.attributes && s.attributes.url;
1018
+ if (!url) continue;
1019
+ const parsed = AppStoreConnectClient.parseDelimited(
1020
+ await client.downloadUrl(url),
1021
+ );
1022
+ if (!columns.length) columns = parsed.columns;
1023
+ allRows.push(...parsed.rows);
1024
+ }
1025
+ const limit = a.limit ?? 200;
1026
+ return {
1027
+ columns,
1028
+ segments: segs.length,
1029
+ rowCount: allRows.length,
1030
+ returned: Math.min(allRows.length, limit),
1031
+ truncated: allRows.length > limit,
1032
+ rows: allRows.slice(0, limit),
1033
+ };
1034
+ },
1035
+ },
1036
+
754
1037
  // ---- Generic escape hatch ----
755
1038
  {
756
1039
  name: "raw_request",
@@ -806,6 +1089,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
806
1089
  const result = await tool.run(req.params.arguments || {});
807
1090
  return ok(result);
808
1091
  } catch (e) {
1092
+ if (e && e.status === 403 && REPORT_TOOLS.has(req.params.name))
1093
+ e.message += ROLE_HINT;
809
1094
  return fail(e);
810
1095
  }
811
1096
  });