appstore-api-mcp 1.16.2 → 1.16.4

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 CHANGED
@@ -4,6 +4,28 @@ 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.16.4] - 2026-10-08
8
+
9
+ ### Fixed
10
+ - `apply_ppp_prices` could never apply to an **in-app purchase**: it defaulted
11
+ `preserveCurrentPrice` to true and passed it into the resolver, which rejects
12
+ IAPs when that flag is set ("applies only to subscriptions"). The default is
13
+ now applied only when calling the subscription apply path (a no-op for IAPs),
14
+ after resolution, so IAP PPP pricing works. (Workaround on older versions:
15
+ pass `preserveCurrentPrice:false` for IAPs.)
16
+
17
+ ## [1.16.3] - 2026-10-08
18
+
19
+ ### Fixed
20
+ - **Wrong US price for products with a price change** (affects `list_purchasable_products`
21
+ and the PPP base price in `preview_ppp_prices` / `apply_ppp_prices`). The US-price
22
+ lookup returned the *first* USA price row — usually the original base price —
23
+ instead of the price in effect today. A subscription raised from $0.99 to $2.99
24
+ reported $0.99, which also skewed every per-territory PPP recommendation derived
25
+ from it. Now selects the current effective price (latest `startDate` on or before
26
+ today; `null` = baseline; future-scheduled changes ignored) for both subscriptions
27
+ and IAPs. New pure `currentEffectivePrice()` helper, unit-tested.
28
+
7
29
  ## [1.16.2] - 2026-10-08
8
30
 
9
31
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-api-mcp",
3
- "version": "1.16.2",
3
+ "version": "1.16.4",
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
@@ -32,6 +32,7 @@ import {
32
32
  decodeTerritory,
33
33
  validateCoefficients,
34
34
  tierSummary,
35
+ currentEffectivePrice,
35
36
  } from "./ppp.js";
36
37
  import { detectOverlappingOffers } from "./subscriptions.js";
37
38
 
@@ -431,13 +432,16 @@ async function pppFetchUsPrice(product) {
431
432
  const ppMap = new Map(
432
433
  included.filter((i) => i.type === "subscriptionPricePoints").map((i) => [i.id, i]),
433
434
  );
435
+ // Collect ALL USA price rows (original + every scheduled change) with their
436
+ // startDate, then pick the one effective today — not just the first row,
437
+ // which is usually the stale original base price.
438
+ const rows = [];
434
439
  for (const price of data) {
435
440
  if (price.relationships?.territory?.data?.id !== "USA") continue;
436
- const ppId = price.relationships?.subscriptionPricePoint?.data?.id;
437
- const pp = ppMap.get(ppId);
438
- if (pp) return Number(pp.attributes.customerPrice);
441
+ const pp = ppMap.get(price.relationships?.subscriptionPricePoint?.data?.id);
442
+ if (pp) rows.push({ startDate: price.attributes?.startDate ?? null, amount: pp.attributes.customerPrice });
439
443
  }
440
- return null;
444
+ return currentEffectivePrice(rows);
441
445
  }
442
446
  const { data, included } = await client.getAllPages(
443
447
  `/inAppPurchasePriceSchedules/${product.id}/manualPrices`,
@@ -449,15 +453,14 @@ async function pppFetchUsPrice(product) {
449
453
  const ppMap = new Map(
450
454
  included.filter((i) => i.type === "inAppPurchasePricePoints").map((i) => [i.id, i]),
451
455
  );
456
+ const rows = [];
452
457
  for (const price of data) {
453
- if (price.attributes?.startDate != null) continue; // skip scheduled future prices
454
458
  const ppId = price.relationships?.inAppPurchasePricePoint?.data?.id || "";
455
- if (decodeTerritory(ppId) === "USA") {
456
- const pp = ppMap.get(ppId);
457
- if (pp) return Number(pp.attributes.customerPrice);
458
- }
459
+ if (decodeTerritory(ppId) !== "USA") continue;
460
+ const pp = ppMap.get(ppId);
461
+ if (pp) rows.push({ startDate: price.attributes?.startDate ?? null, amount: pp.attributes.customerPrice });
459
462
  }
460
- return null;
463
+ return currentEffectivePrice(rows);
461
464
  }
462
465
 
463
466
  /** Parse raw price-point rows into { id, customerPrice, territory3 }, skipping bad ones. */
@@ -3930,10 +3933,14 @@ ${a.teamId ? `<key>teamID</key><string>${a.teamId}</string>\n` : ""}<key>uploadS
3930
3933
  error:
3931
3934
  "Refused: apply_ppp_prices needs confirm:true. Run preview_ppp_prices, show the user the table, get explicit approval, then re-call with confirm:true.",
3932
3935
  };
3933
- // Default preserveCurrentPrice to true for subscriptions (safer for existing subscribers).
3934
- const preserved = a.preserveCurrentPrice === undefined ? true : a.preserveCurrentPrice;
3935
- const r = await pppResolve({ ...a, preserveCurrentPrice: preserved });
3936
+ // Resolve with the user's args as-is. Do NOT inject a default
3937
+ // preserveCurrentPrice here: the resolver rejects IAPs when that flag is
3938
+ // set, so defaulting it to true before resolve broke IAP applies.
3939
+ const r = await pppResolve(a);
3936
3940
  if (r.error) return r;
3941
+ // Default preserveCurrentPrice to true for subscriptions only (safer for
3942
+ // existing subscribers); it is a no-op for IAPs.
3943
+ const preserved = a.preserveCurrentPrice === undefined ? true : a.preserveCurrentPrice;
3937
3944
  const result = await pppApply(r.product, r.resolved, {
3938
3945
  preserved,
3939
3946
  startDate: a.startDate,
@@ -5738,7 +5745,7 @@ Some things a great App Store listing needs live OUTSIDE App Store Connect and c
5738
5745
  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.`;
5739
5746
 
5740
5747
  const server = new Server(
5741
- { name: "appstore-api-mcp", version: "1.16.2" },
5748
+ { name: "appstore-api-mcp", version: "1.16.4" },
5742
5749
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },
5743
5750
  );
5744
5751
 
package/src/ppp.js CHANGED
@@ -168,6 +168,28 @@ export function decodeTerritory(pricePointId) {
168
168
  }
169
169
  }
170
170
 
171
+ /**
172
+ * Pick the CURRENTLY-EFFECTIVE price from dated price rows.
173
+ * rows: [{ startDate: string|null, amount: number }]
174
+ * App Store Connect keeps every price a product has ever had: the original
175
+ * (startDate null) plus one row per scheduled change. The price customers pay
176
+ * today is the row with the LATEST startDate that is on or before today; a null
177
+ * startDate is the baseline (effective from the beginning). Future-dated rows
178
+ * (a scheduled upcoming change) are ignored. Returns the amount, or null.
179
+ */
180
+ export function currentEffectivePrice(rows, today = new Date().toISOString().slice(0, 10)) {
181
+ let best = null;
182
+ for (const r of rows || []) {
183
+ const amount = Number(r.amount);
184
+ if (!Number.isFinite(amount)) continue;
185
+ const start = r.startDate || null;
186
+ if (start && start > today) continue; // skip not-yet-active scheduled prices
187
+ const key = start || ""; // null baseline sorts earliest
188
+ if (!best || key >= best.key) best = { key, amount };
189
+ }
190
+ return best ? best.amount : null;
191
+ }
192
+
171
193
  /** The default coefficient + floor for each tier, for tool docs / previews. */
172
194
  export function tierSummary() {
173
195
  const seen = new Map();