pabal-store-api-mcp 1.3.10 → 1.3.13

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.
@@ -7,6 +7,10 @@ interface AppStoreAppInfo {
7
7
  name?: string;
8
8
  supportedLocales?: string[];
9
9
  }
10
+ export declare function resolveAppStoreLocales(allLocales: string[], requestedLocales?: string[]): {
11
+ localesToPush: string[];
12
+ missingLocales: string[];
13
+ };
10
14
  /**
11
15
  * App Store-facing service layer that wraps client creation and common operations.
12
16
  * Keeps MCP tools independent from client factories and SDK details.
@@ -26,12 +30,14 @@ export declare class AppStoreService {
26
30
  updateReleaseNotes(bundleId: string, releaseNotes: Record<string, string>, versionId?: string, supportedLocales?: string[]): Promise<ServiceResult<UpdatedReleaseNotesResult>>;
27
31
  pullReleaseNotes(bundleId: string): Promise<ServiceResult<AppStoreReleaseNote[]>>;
28
32
  createVersion(bundleId: string, versionString: string, autoIncrement?: boolean): Promise<ServiceResult<CreatedAppStoreVersion>>;
29
- pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages, slug, }: {
33
+ pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages, locales, imageUploadTimeoutMs, slug, }: {
30
34
  config: EnvConfig;
31
35
  bundleId?: string;
32
36
  localAsoData: AsoData;
33
37
  appStoreDataPath: string;
34
38
  uploadImages?: boolean;
39
+ locales?: string[];
40
+ imageUploadTimeoutMs?: number;
35
41
  slug?: string;
36
42
  }): Promise<PushAsoResult>;
37
43
  verifyAuth(expirationSeconds?: number): Promise<VerifyAuthResult<{
@@ -6,6 +6,16 @@ import { verifyAppStoreAuth } from "../../packages/stores/app-store/verify-auth.
6
6
  import { createAppStoreClient } from "../../core/clients/app-store-factory.js";
7
7
  import { parseAppStoreScreenshots, hasScreenshots, APP_STORE_DEVICE_TYPES, } from "../../core/helpers/screenshot-helpers.js";
8
8
  import { checkPushPrerequisites, serviceFailure, toServiceResult, updateRegisteredLocales, } from "./service-helpers.js";
9
+ export function resolveAppStoreLocales(allLocales, requestedLocales) {
10
+ if (!requestedLocales?.length) {
11
+ return { localesToPush: allLocales, missingLocales: [] };
12
+ }
13
+ const requested = new Set(requestedLocales);
14
+ return {
15
+ localesToPush: allLocales.filter((locale) => requested.has(locale)),
16
+ missingLocales: requestedLocales.filter((locale) => !allLocales.includes(locale)),
17
+ };
18
+ }
9
19
  /**
10
20
  * App Store-facing service layer that wraps client creation and common operations.
11
21
  * Keeps MCP tools independent from client factories and SDK details.
@@ -163,7 +173,7 @@ export class AppStoreService {
163
173
  return serviceFailure(AppError.wrap(error, HTTP_STATUS.INTERNAL_SERVER_ERROR, ERROR_CODES.APP_STORE_CREATE_VERSION_FAILED, "Failed to create App Store version"));
164
174
  }
165
175
  }
166
- async pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages = false, slug, }) {
176
+ async pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages = false, locales, imageUploadTimeoutMs, slug, }) {
167
177
  const skip = checkPushPrerequisites({
168
178
  storeLabel: "App Store",
169
179
  configured: Boolean(config.appStore),
@@ -181,9 +191,20 @@ export class AppStoreService {
181
191
  console.error(`[MCP] Bundle ID: ${bundleId}`);
182
192
  try {
183
193
  // Push locale data (supportUrl/marketingUrl already set by prepareAsoDataForPush)
184
- const localesToPush = Object.keys(appStoreData.locales);
194
+ const allLocales = Object.keys(appStoreData.locales);
195
+ const { localesToPush, missingLocales } = resolveAppStoreLocales(allLocales, locales);
196
+ if (missingLocales.length) {
197
+ console.error(`[AppStore] ⚠️ Requested locale(s) not found in local ASO data: ${missingLocales.join(", ")}`);
198
+ }
199
+ if (localesToPush.length === 0) {
200
+ return {
201
+ success: false,
202
+ error: AppError.validation(ERROR_CODES.APP_STORE_ASO_DATA_EMPTY, "No matching App Store locales found to push"),
203
+ };
204
+ }
185
205
  const failedFieldsList = [];
186
- for (const [locale, localeData] of Object.entries(appStoreData.locales)) {
206
+ for (const locale of localesToPush) {
207
+ const localeData = appStoreData.locales[locale];
187
208
  console.error(`[AppStore] 📤 Pushing ${locale}...`);
188
209
  const localeResult = await client.pushAsoData(localeData);
189
210
  if (localeResult.failedFields && localeResult.failedFields.length > 0) {
@@ -279,6 +300,7 @@ export class AppStoreService {
279
300
  const uploadResult = await client.uploadScreenshotsForLocale({
280
301
  locale,
281
302
  screenshots: screenshotsToUpload,
303
+ imageUploadTimeoutMs,
282
304
  });
283
305
  if (uploadResult.failed > 0) {
284
306
  throw new Error(`Screenshot upload reported ${uploadResult.failed} failed files`);
@@ -335,7 +357,6 @@ export class AppStoreService {
335
357
  const version = await client.createNewVersionWithAutoIncrement();
336
358
  const versionId = version.id;
337
359
  const versionString = version.attributes?.versionString ?? "";
338
- const locales = Object.keys(appStoreData.locales);
339
360
  console.error(`[AppStore] ✅ New version ${versionString} created.`);
340
361
  return {
341
362
  success: false,
@@ -344,7 +365,7 @@ export class AppStoreService {
344
365
  versionInfo: {
345
366
  versionId,
346
367
  versionString,
347
- locales,
368
+ locales: Object.keys(appStoreData.locales),
348
369
  },
349
370
  };
350
371
  }
@@ -6,6 +6,14 @@ interface GooglePlayAppInfo {
6
6
  name?: string;
7
7
  supportedLocales?: string[];
8
8
  }
9
+ export declare function resolveGooglePlayLocales(allLocales: string[], requestedLocales?: string[]): {
10
+ localesToPush: string[];
11
+ missingLocales: string[];
12
+ };
13
+ export declare function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }: {
14
+ hasContactDetails: boolean;
15
+ requestedLocales?: string[];
16
+ }): boolean;
9
17
  /**
10
18
  * Google Play-facing service layer that wraps client creation and common operations.
11
19
  * Keeps MCP tools independent from client factories and SDK details.
@@ -21,12 +29,14 @@ export declare class GooglePlayService {
21
29
  updateReleaseNotes(packageName: string, releaseNotes: Record<string, string>, track?: string, supportedLocales?: string[]): Promise<ServiceResult<UpdatedReleaseNotesResult>>;
22
30
  pullReleaseNotes(packageName: string): Promise<ServiceResult<GooglePlayReleaseNote[]>>;
23
31
  createVersion(packageName: string, versionString: string, versionCodes: number[]): Promise<ServiceResult<CreatedGooglePlayVersion>>;
24
- pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages, slug, }: {
32
+ pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages, locales, imageUploadTimeoutMs, slug, }: {
25
33
  config: EnvConfig;
26
34
  packageName?: string;
27
35
  localAsoData: AsoData;
28
36
  googlePlayDataPath: string;
29
37
  uploadImages?: boolean;
38
+ locales?: string[];
39
+ imageUploadTimeoutMs?: number;
30
40
  slug?: string;
31
41
  }): Promise<PushAsoResult>;
32
42
  verifyAuth(): Promise<VerifyAuthResult<{
@@ -6,6 +6,20 @@ import { verifyPlayStoreAuth } from "../../packages/stores/play-store/verify-aut
6
6
  import { createGooglePlayClient } from "../../core/clients/google-play-factory.js";
7
7
  import { parseGooglePlayScreenshots, hasScreenshots, } from "../../core/helpers/screenshot-helpers.js";
8
8
  import { checkPushPrerequisites, serviceFailure, toServiceResult, updateRegisteredLocales, } from "./service-helpers.js";
9
+ const GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE = 5;
10
+ export function resolveGooglePlayLocales(allLocales, requestedLocales) {
11
+ if (!requestedLocales?.length) {
12
+ return { localesToPush: allLocales, missingLocales: [] };
13
+ }
14
+ const requested = new Set(requestedLocales);
15
+ return {
16
+ localesToPush: allLocales.filter((locale) => requested.has(locale)),
17
+ missingLocales: requestedLocales.filter((locale) => !allLocales.includes(locale)),
18
+ };
19
+ }
20
+ export function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }) {
21
+ return hasContactDetails && !requestedLocales?.length;
22
+ }
9
23
  /**
10
24
  * Google Play-facing service layer that wraps client creation and common operations.
11
25
  * Keeps MCP tools independent from client factories and SDK details.
@@ -148,7 +162,7 @@ export class GooglePlayService {
148
162
  return serviceFailure(AppError.wrap(error, HTTP_STATUS.INTERNAL_SERVER_ERROR, ERROR_CODES.GOOGLE_PLAY_CREATE_VERSION_FAILED, "Failed to create Google Play version"));
149
163
  }
150
164
  }
151
- async pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages = false, slug, }) {
165
+ async pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages = false, locales, imageUploadTimeoutMs, slug, }) {
152
166
  const skip = checkPushPrerequisites({
153
167
  storeLabel: "Google Play",
154
168
  configured: Boolean(config.playStore),
@@ -165,14 +179,35 @@ export class GooglePlayService {
165
179
  console.error(`[MCP] 📤 Pushing to Google Play...`);
166
180
  console.error(`[MCP] Package: ${packageName}`);
167
181
  try {
168
- const localesToPush = Object.keys(googlePlayData.locales);
182
+ const allLocales = Object.keys(googlePlayData.locales);
183
+ const { localesToPush, missingLocales } = resolveGooglePlayLocales(allLocales, locales);
184
+ if (missingLocales.length) {
185
+ console.error(`[GooglePlay] ⚠️ Requested locale(s) not found in local ASO data: ${missingLocales.join(", ")}`);
186
+ }
187
+ if (localesToPush.length === 0) {
188
+ return {
189
+ success: false,
190
+ error: AppError.validation(ERROR_CODES.GOOGLE_PLAY_ASO_DATA_EMPTY, "No matching Google Play locales found to push"),
191
+ };
192
+ }
169
193
  for (const locale of localesToPush) {
170
194
  console.error(`[GooglePlay] 📤 Preparing locale: ${locale}`);
171
195
  }
172
196
  // Push locale data as-is from aso-data.json
173
- await client.pushMultilingualAsoData(googlePlayData);
174
- // Push app-level contact information
175
- if (googlePlayData.contactEmail || googlePlayData.contactWebsite) {
197
+ await client.pushMultilingualAsoData({
198
+ ...googlePlayData,
199
+ locales: Object.fromEntries(localesToPush.map((locale) => [
200
+ locale,
201
+ googlePlayData.locales[locale],
202
+ ])),
203
+ });
204
+ // Push app-level contact information once for full pushes. Partial locale
205
+ // pushes are commonly batched, and repeating details edits can invalidate
206
+ // otherwise-successful listing commits on Google Play.
207
+ if (shouldPushGooglePlayAppDetails({
208
+ hasContactDetails: Boolean(googlePlayData.contactEmail || googlePlayData.contactWebsite),
209
+ requestedLocales: locales,
210
+ })) {
176
211
  console.error(`[GooglePlay] 📤 Pushing app details...`);
177
212
  await client.pushAppDetails({
178
213
  contactEmail: googlePlayData.contactEmail,
@@ -180,6 +215,9 @@ export class GooglePlayService {
180
215
  });
181
216
  console.error(`[GooglePlay] ✅ App details uploaded successfully`);
182
217
  }
218
+ else if (locales?.length) {
219
+ console.error(`[GooglePlay] ⏭️ Skipping app details for partial locale push`);
220
+ }
183
221
  // Upload screenshots if enabled
184
222
  if (uploadImages && slug) {
185
223
  console.error(`[GooglePlay] 📤 Uploading screenshots...`);
@@ -188,6 +226,7 @@ export class GooglePlayService {
188
226
  const uploadedLocales = [];
189
227
  const skippedLocales = [];
190
228
  const failedLocales = [];
229
+ const screenshotUploadOptions = [];
191
230
  for (const locale of localesToPush) {
192
231
  try {
193
232
  const localeData = googlePlayData.locales[locale];
@@ -245,25 +284,40 @@ export class GooglePlayService {
245
284
  skippedLocales.push(locale);
246
285
  continue;
247
286
  }
248
- console.error(`[GooglePlay] 📤 Uploading screenshots for ${locale} (batch mode - will replace existing)...`);
287
+ console.error(`[GooglePlay] 📋 Queued screenshots for ${locale} (batch mode - will replace existing)...`);
249
288
  // Google Play upload strategy:
250
289
  // - phone → uploads to phoneScreenshots AND sevenInchScreenshots (both use same images)
251
290
  // - tablet → uploads to tenInchScreenshots only
252
- const uploadResult = await client.uploadScreenshotsForLocale({
291
+ screenshotUploadOptions.push({
253
292
  language: locale,
254
293
  phoneScreenshots: screenshots.phone,
255
294
  sevenInchScreenshots: screenshots.phone,
256
295
  tenInchScreenshots: screenshots.tablet,
257
296
  featureGraphic: screenshots.featureGraphic || undefined,
297
+ imageUploadTimeoutMs,
258
298
  });
259
- console.error(`[GooglePlay] ✅ Images uploaded for ${locale}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
260
- uploadedLocales.push(locale);
261
299
  }
262
300
  catch (error) {
263
301
  console.error(`[GooglePlay] ❌ Failed to upload screenshots for ${locale}: ${error instanceof Error ? error.message : String(error)}`);
264
302
  failedLocales.push(locale);
265
303
  }
266
304
  }
305
+ for (let offset = 0; offset < screenshotUploadOptions.length; offset += GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE) {
306
+ const batch = screenshotUploadOptions.slice(offset, offset + GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE);
307
+ try {
308
+ console.error(`[GooglePlay] 📤 Uploading screenshots for ${batch.length} locale(s) in one edit...`);
309
+ const uploadResults = await client.uploadScreenshotsForLocales(batch);
310
+ for (const uploadResult of uploadResults) {
311
+ console.error(`[GooglePlay] ✅ Images uploaded for ${uploadResult.language}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
312
+ uploadedLocales.push(uploadResult.language);
313
+ }
314
+ }
315
+ catch (error) {
316
+ const failedBatchLocales = batch.map((option) => option.language);
317
+ console.error(`[GooglePlay] ❌ Batch screenshot upload failed for ${failedBatchLocales.join(", ")}: ${error instanceof Error ? error.message : String(error)}`);
318
+ failedLocales.push(...failedBatchLocales);
319
+ }
320
+ }
267
321
  console.error(`[GooglePlay] 📊 Screenshot upload summary: ${uploadedLocales.length} succeeded, ${skippedLocales.length} skipped, ${failedLocales.length} failed`);
268
322
  if (uploadedLocales.length > 0) {
269
323
  console.error(`[GooglePlay] ✅ Uploaded: ${uploadedLocales.join(", ")}`);
@@ -272,7 +326,9 @@ export class GooglePlayService {
272
326
  console.error(`[GooglePlay] ⏭️ Skipped: ${skippedLocales.join(", ")}`);
273
327
  }
274
328
  if (failedLocales.length > 0) {
275
- console.error(`[GooglePlay] ❌ Failed: ${failedLocales.join(", ")}`);
329
+ const uniqueFailedLocales = [...new Set(failedLocales)];
330
+ console.error(`[GooglePlay] ❌ Failed: ${uniqueFailedLocales.join(", ")}`);
331
+ throw new Error(`Screenshot upload failed for locales: ${uniqueFailedLocales.join(", ")}`);
276
332
  }
277
333
  }
278
334
  try {
package/dist/src/index.js CHANGED
@@ -195,6 +195,16 @@ registerToolWithInfo("aso-push", {
195
195
  .boolean()
196
196
  .optional()
197
197
  .describe("Whether to upload images as well"),
198
+ locales: z
199
+ .array(z.string())
200
+ .optional()
201
+ .describe("Optional locale allowlist to push (e.g. ['en-US', 'ko-KR'])"),
202
+ imageUploadTimeoutMs: z
203
+ .number()
204
+ .int()
205
+ .positive()
206
+ .optional()
207
+ .describe("Per-image upload timeout in milliseconds"),
198
208
  dryRun: z
199
209
  .boolean()
200
210
  .optional()
@@ -42,6 +42,7 @@ export declare const ERROR_CODES: {
42
42
  readonly APP_STORE_PULL_RELEASE_NOTES_FAILED: "APP_STORE_PULL_RELEASE_NOTES_FAILED";
43
43
  readonly APP_STORE_CREATE_VERSION_FAILED: "APP_STORE_CREATE_VERSION_FAILED";
44
44
  readonly APP_STORE_PUSH_FAILED: "APP_STORE_PUSH_FAILED";
45
+ readonly APP_STORE_ASO_DATA_EMPTY: "APP_STORE_ASO_DATA_EMPTY";
45
46
  readonly APP_STORE_STATE_ERROR: "APP_STORE_STATE_ERROR";
46
47
  readonly APP_STORE_CREATE_VERSION_FOR_STATE_ERROR_FAILED: "APP_STORE_CREATE_VERSION_FOR_STATE_ERROR_FAILED";
47
48
  readonly APP_STORE_VERIFY_AUTH_FAILED: "APP_STORE_VERIFY_AUTH_FAILED";
@@ -53,6 +54,7 @@ export declare const ERROR_CODES: {
53
54
  readonly GOOGLE_PLAY_PULL_RELEASE_NOTES_FAILED: "GOOGLE_PLAY_PULL_RELEASE_NOTES_FAILED";
54
55
  readonly GOOGLE_PLAY_CREATE_VERSION_FAILED: "GOOGLE_PLAY_CREATE_VERSION_FAILED";
55
56
  readonly GOOGLE_PLAY_PUSH_FAILED: "GOOGLE_PLAY_PUSH_FAILED";
57
+ readonly GOOGLE_PLAY_ASO_DATA_EMPTY: "GOOGLE_PLAY_ASO_DATA_EMPTY";
56
58
  readonly GOOGLE_PLAY_VERIFY_AUTH_FAILED: "GOOGLE_PLAY_VERIFY_AUTH_FAILED";
57
59
  readonly ASO_GOOGLE_PLAY_DATA_PARSE_FAILED: "ASO_GOOGLE_PLAY_DATA_PARSE_FAILED";
58
60
  readonly ASO_APP_STORE_DATA_PARSE_FAILED: "ASO_APP_STORE_DATA_PARSE_FAILED";
@@ -49,6 +49,7 @@ export const ERROR_CODES = {
49
49
  APP_STORE_PULL_RELEASE_NOTES_FAILED: "APP_STORE_PULL_RELEASE_NOTES_FAILED",
50
50
  APP_STORE_CREATE_VERSION_FAILED: "APP_STORE_CREATE_VERSION_FAILED",
51
51
  APP_STORE_PUSH_FAILED: "APP_STORE_PUSH_FAILED",
52
+ APP_STORE_ASO_DATA_EMPTY: "APP_STORE_ASO_DATA_EMPTY",
52
53
  APP_STORE_STATE_ERROR: "APP_STORE_STATE_ERROR",
53
54
  APP_STORE_CREATE_VERSION_FOR_STATE_ERROR_FAILED: "APP_STORE_CREATE_VERSION_FOR_STATE_ERROR_FAILED",
54
55
  APP_STORE_VERIFY_AUTH_FAILED: "APP_STORE_VERIFY_AUTH_FAILED",
@@ -61,6 +62,7 @@ export const ERROR_CODES = {
61
62
  GOOGLE_PLAY_PULL_RELEASE_NOTES_FAILED: "GOOGLE_PLAY_PULL_RELEASE_NOTES_FAILED",
62
63
  GOOGLE_PLAY_CREATE_VERSION_FAILED: "GOOGLE_PLAY_CREATE_VERSION_FAILED",
63
64
  GOOGLE_PLAY_PUSH_FAILED: "GOOGLE_PLAY_PUSH_FAILED",
65
+ GOOGLE_PLAY_ASO_DATA_EMPTY: "GOOGLE_PLAY_ASO_DATA_EMPTY",
64
66
  GOOGLE_PLAY_VERIFY_AUTH_FAILED: "GOOGLE_PLAY_VERIFY_AUTH_FAILED",
65
67
  // ASO data/files
66
68
  ASO_GOOGLE_PLAY_DATA_PARSE_FAILED: "ASO_GOOGLE_PLAY_DATA_PARSE_FAILED",
@@ -82,6 +82,7 @@ export declare class AppStoreClient {
82
82
  imagePath: string;
83
83
  screenshotDisplayType: string;
84
84
  locale: string;
85
+ imageUploadTimeoutMs?: number;
85
86
  }): Promise<void>;
86
87
  /**
87
88
  * Find or create Screenshot Set for a specific display type
@@ -124,6 +125,7 @@ export declare class AppStoreClient {
124
125
  displayType: string;
125
126
  filename: string;
126
127
  }>;
128
+ imageUploadTimeoutMs?: number;
127
129
  }): Promise<{
128
130
  uploaded: number;
129
131
  deleted: number;
@@ -577,7 +577,7 @@ export class AppStoreClient {
577
577
  * 4. Commit upload operation
578
578
  */
579
579
  async uploadScreenshot(options) {
580
- const { imagePath, screenshotDisplayType, locale } = options;
580
+ const { imagePath, screenshotDisplayType, locale, imageUploadTimeoutMs } = options;
581
581
  try {
582
582
  // Get app and version info
583
583
  const appId = await this.findAppId();
@@ -611,7 +611,7 @@ export class AppStoreClient {
611
611
  if (screenshot.uploadOperations &&
612
612
  screenshot.uploadOperations.length > 0) {
613
613
  const uploadOp = screenshot.uploadOperations[0];
614
- await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method);
614
+ await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method, imageUploadTimeoutMs);
615
615
  }
616
616
  // Step 4: Commit screenshot
617
617
  await this.commitAppScreenshot(screenshot.id);
@@ -703,7 +703,7 @@ export class AppStoreClient {
703
703
  /**
704
704
  * Upload file to reserved URL
705
705
  */
706
- async uploadFileToUrl(url, fileBuffer, method = "PUT") {
706
+ async uploadFileToUrl(url, fileBuffer, method = "PUT", timeoutMs) {
707
707
  const https = await import("node:https");
708
708
  const { URL } = await import("node:url");
709
709
  return new Promise((resolve, reject) => {
@@ -727,6 +727,11 @@ export class AppStoreClient {
727
727
  }
728
728
  });
729
729
  req.on("error", reject);
730
+ if (timeoutMs) {
731
+ req.setTimeout(timeoutMs, () => {
732
+ req.destroy(new Error(`Upload timed out after ${timeoutMs}ms`));
733
+ });
734
+ }
730
735
  req.write(fileBuffer);
731
736
  req.end();
732
737
  });
@@ -776,6 +781,10 @@ export class AppStoreClient {
776
781
  const screenshots = screenshotsResponse.data || [];
777
782
  let deletedCount = 0;
778
783
  for (const screenshot of screenshots) {
784
+ if (screenshot.type !== "appScreenshots") {
785
+ console.error(`[AppStore] Skipping non-screenshot asset ${screenshot.id}`);
786
+ continue;
787
+ }
779
788
  await this.deleteScreenshot(screenshot.id);
780
789
  deletedCount++;
781
790
  }
@@ -853,7 +862,7 @@ export class AppStoreClient {
853
862
  * 3. Upload new screenshots in order
854
863
  */
855
864
  async uploadScreenshotsForLocale(options) {
856
- const { locale, screenshots } = options;
865
+ const { locale, screenshots, imageUploadTimeoutMs } = options;
857
866
  const result = { uploaded: 0, deleted: 0, failed: 0 };
858
867
  if (screenshots.length === 0) {
859
868
  return result;
@@ -914,7 +923,7 @@ export class AppStoreClient {
914
923
  if (screenshotData.uploadOperations &&
915
924
  screenshotData.uploadOperations.length > 0) {
916
925
  const uploadOp = screenshotData.uploadOperations[0];
917
- await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method);
926
+ await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method, imageUploadTimeoutMs);
918
927
  }
919
928
  // Commit screenshot
920
929
  await this.commitAppScreenshot(screenshotData.id);
@@ -55,6 +55,9 @@ export declare class GooglePlayClient {
55
55
  * Deletes existing screenshots before uploading new ones
56
56
  */
57
57
  uploadScreenshotsForLocale(options: BatchUploadScreenshotsOptions): Promise<BatchUploadScreenshotsResult>;
58
+ uploadScreenshotsForLocales(optionsList: BatchUploadScreenshotsOptions[]): Promise<BatchUploadScreenshotsResult[]>;
59
+ private uploadScreenshotsInSession;
60
+ private uploadImageWithOptionalTimeout;
58
61
  private getTrack;
59
62
  private updateTrack;
60
63
  private listTracks;
@@ -620,7 +620,6 @@ export class GooglePlayClient {
620
620
  * Deletes existing screenshots before uploading new ones
621
621
  */
622
622
  async uploadScreenshotsForLocale(options) {
623
- const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, } = options;
624
623
  const authClient = await this.auth.getClient();
625
624
  const editResponse = await this.createEdit(authClient, this.packageName);
626
625
  const editId = editResponse.data.id;
@@ -629,6 +628,61 @@ export class GooglePlayClient {
629
628
  packageName: this.packageName,
630
629
  editId,
631
630
  };
631
+ try {
632
+ const result = await this.uploadScreenshotsInSession(session, options);
633
+ // Commit all changes
634
+ console.error(`[GooglePlayClient] Committing screenshots for ${options.language}...`);
635
+ await this.commitEdit(session);
636
+ console.error(`[GooglePlayClient] ✅ Screenshots committed for ${options.language}`);
637
+ return result;
638
+ }
639
+ catch (error) {
640
+ console.error(`[GooglePlayClient] Rolling back screenshot upload for ${options.language}...`);
641
+ try {
642
+ await this.deleteEdit(session);
643
+ }
644
+ catch {
645
+ // Ignore deletion failure
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ async uploadScreenshotsForLocales(optionsList) {
651
+ if (optionsList.length === 0)
652
+ return [];
653
+ const authClient = await this.auth.getClient();
654
+ const editResponse = await this.createEdit(authClient, this.packageName);
655
+ const editId = editResponse.data.id;
656
+ const session = {
657
+ auth: authClient,
658
+ packageName: this.packageName,
659
+ editId,
660
+ };
661
+ try {
662
+ const results = [];
663
+ for (const options of optionsList) {
664
+ console.error(`[GooglePlayClient] Preparing screenshots for ${options.language}...`);
665
+ const result = await this.uploadScreenshotsInSession(session, options);
666
+ results.push(result);
667
+ }
668
+ console.error(`[GooglePlayClient] Committing screenshots for ${optionsList.length} locale(s)...`);
669
+ await this.commitEdit(session);
670
+ console.error(`[GooglePlayClient] ✅ Screenshots committed for ${optionsList.length} locale(s)`);
671
+ return results;
672
+ }
673
+ catch (error) {
674
+ console.error(`[GooglePlayClient] Rolling back batch screenshot upload...`);
675
+ try {
676
+ await this.deleteEdit(session);
677
+ }
678
+ catch {
679
+ // Ignore deletion failure
680
+ }
681
+ throw error;
682
+ }
683
+ }
684
+ async uploadScreenshotsInSession(session, options) {
685
+ const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, imageUploadTimeoutMs, } = options;
632
686
  const result = {
633
687
  language,
634
688
  uploaded: {
@@ -638,155 +692,107 @@ export class GooglePlayClient {
638
692
  featureGraphic: false,
639
693
  },
640
694
  };
641
- try {
642
- // Delete existing screenshots before uploading
643
- if (phoneScreenshots.length > 0) {
644
- console.error(`[GooglePlayClient] Deleting existing phone screenshots for ${language}...`);
645
- try {
646
- await this.deleteAllImages(session, language, "phoneScreenshots");
647
- }
648
- catch (e) {
649
- // Ignore if no images exist
650
- if (e.code !== 404) {
651
- console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
652
- }
653
- }
695
+ if (phoneScreenshots.length > 0) {
696
+ console.error(`[GooglePlayClient] Deleting existing phone screenshots for ${language}...`);
697
+ try {
698
+ await this.deleteAllImages(session, language, "phoneScreenshots");
654
699
  }
655
- if (sevenInchScreenshots.length > 0) {
656
- console.error(`[GooglePlayClient] Deleting existing 7-inch screenshots for ${language}...`);
657
- try {
658
- await this.deleteAllImages(session, language, "sevenInchScreenshots");
659
- }
660
- catch (e) {
661
- if (e.code !== 404) {
662
- console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
663
- }
700
+ catch (e) {
701
+ // Ignore if no images exist
702
+ if (e.code !== 404) {
703
+ console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
664
704
  }
665
705
  }
666
- if (tenInchScreenshots.length > 0) {
667
- console.error(`[GooglePlayClient] Deleting existing 10-inch screenshots for ${language}...`);
668
- try {
669
- await this.deleteAllImages(session, language, "tenInchScreenshots");
670
- }
671
- catch (e) {
672
- if (e.code !== 404) {
673
- console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
674
- }
675
- }
706
+ }
707
+ if (sevenInchScreenshots.length > 0) {
708
+ console.error(`[GooglePlayClient] Deleting existing 7-inch screenshots for ${language}...`);
709
+ try {
710
+ await this.deleteAllImages(session, language, "sevenInchScreenshots");
676
711
  }
677
- if (featureGraphic) {
678
- console.error(`[GooglePlayClient] Deleting existing feature graphic for ${language}...`);
679
- try {
680
- await this.deleteAllImages(session, language, "featureGraphic");
681
- }
682
- catch (e) {
683
- if (e.code !== 404) {
684
- console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
685
- }
712
+ catch (e) {
713
+ if (e.code !== 404) {
714
+ console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
686
715
  }
687
716
  }
688
- // Upload phone screenshots
689
- for (let i = 0; i < phoneScreenshots.length; i++) {
690
- const imagePath = phoneScreenshots[i];
691
- if (!existsSync(imagePath)) {
692
- console.error(`[GooglePlayClient] Warning: Phone screenshot not found: ${imagePath}`);
693
- continue;
694
- }
695
- const imageBuffer = readFileSync(imagePath);
696
- const fileName = imagePath.split("/").pop() || `phone-${i + 1}.png`;
697
- await this.androidPublisher.edits.images.upload({
698
- auth: session.auth,
699
- packageName: session.packageName,
700
- editId: session.editId,
701
- language,
702
- imageType: "phoneScreenshots",
703
- media: {
704
- mimeType: "image/png",
705
- body: imageBuffer,
706
- },
707
- });
708
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
709
- result.uploaded.phoneScreenshots++;
710
- }
711
- // Upload 7-inch tablet screenshots
712
- for (let i = 0; i < sevenInchScreenshots.length; i++) {
713
- const imagePath = sevenInchScreenshots[i];
714
- if (!existsSync(imagePath)) {
715
- console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
716
- continue;
717
- }
718
- const imageBuffer = readFileSync(imagePath);
719
- const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
720
- await this.androidPublisher.edits.images.upload({
721
- auth: session.auth,
722
- packageName: session.packageName,
723
- editId: session.editId,
724
- language,
725
- imageType: "sevenInchScreenshots",
726
- media: {
727
- mimeType: "image/png",
728
- body: imageBuffer,
729
- },
730
- });
731
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
732
- result.uploaded.sevenInchScreenshots++;
733
- }
734
- // Upload 10-inch tablet screenshots
735
- for (let i = 0; i < tenInchScreenshots.length; i++) {
736
- const imagePath = tenInchScreenshots[i];
737
- if (!existsSync(imagePath)) {
738
- console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
739
- continue;
717
+ }
718
+ if (tenInchScreenshots.length > 0) {
719
+ console.error(`[GooglePlayClient] Deleting existing 10-inch screenshots for ${language}...`);
720
+ try {
721
+ await this.deleteAllImages(session, language, "tenInchScreenshots");
722
+ }
723
+ catch (e) {
724
+ if (e.code !== 404) {
725
+ console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
740
726
  }
741
- const imageBuffer = readFileSync(imagePath);
742
- const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
743
- await this.androidPublisher.edits.images.upload({
744
- auth: session.auth,
745
- packageName: session.packageName,
746
- editId: session.editId,
747
- language,
748
- imageType: "tenInchScreenshots",
749
- media: {
750
- mimeType: "image/png",
751
- body: imageBuffer,
752
- },
753
- });
754
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
755
- result.uploaded.tenInchScreenshots++;
756
- }
757
- // Upload feature graphic
758
- if (featureGraphic && existsSync(featureGraphic)) {
759
- const imageBuffer = readFileSync(featureGraphic);
760
- await this.androidPublisher.edits.images.upload({
761
- auth: session.auth,
762
- packageName: session.packageName,
763
- editId: session.editId,
764
- language,
765
- imageType: "featureGraphic",
766
- media: {
767
- mimeType: "image/png",
768
- body: imageBuffer,
769
- },
770
- });
771
- console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
772
- result.uploaded.featureGraphic = true;
773
727
  }
774
- // Commit all changes
775
- console.error(`[GooglePlayClient] Committing screenshots for ${language}...`);
776
- await this.commitEdit(session);
777
- console.error(`[GooglePlayClient] ✅ Screenshots committed for ${language}`);
778
- return result;
779
728
  }
780
- catch (error) {
781
- console.error(`[GooglePlayClient] Rolling back screenshot upload for ${language}...`);
729
+ if (featureGraphic) {
730
+ console.error(`[GooglePlayClient] Deleting existing feature graphic for ${language}...`);
782
731
  try {
783
- await this.deleteEdit(session);
732
+ await this.deleteAllImages(session, language, "featureGraphic");
784
733
  }
785
- catch {
786
- // Ignore deletion failure
734
+ catch (e) {
735
+ if (e.code !== 404) {
736
+ console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
737
+ }
787
738
  }
788
- throw error;
789
739
  }
740
+ for (let i = 0; i < phoneScreenshots.length; i++) {
741
+ const imagePath = phoneScreenshots[i];
742
+ if (!existsSync(imagePath)) {
743
+ console.error(`[GooglePlayClient] Warning: Phone screenshot not found: ${imagePath}`);
744
+ continue;
745
+ }
746
+ const imageBuffer = readFileSync(imagePath);
747
+ const fileName = imagePath.split("/").pop() || `phone-${i + 1}.png`;
748
+ await this.uploadImageWithOptionalTimeout(session, language, "phoneScreenshots", imageBuffer, imageUploadTimeoutMs);
749
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
750
+ result.uploaded.phoneScreenshots++;
751
+ }
752
+ for (let i = 0; i < sevenInchScreenshots.length; i++) {
753
+ const imagePath = sevenInchScreenshots[i];
754
+ if (!existsSync(imagePath)) {
755
+ console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
756
+ continue;
757
+ }
758
+ const imageBuffer = readFileSync(imagePath);
759
+ const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
760
+ await this.uploadImageWithOptionalTimeout(session, language, "sevenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
761
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
762
+ result.uploaded.sevenInchScreenshots++;
763
+ }
764
+ for (let i = 0; i < tenInchScreenshots.length; i++) {
765
+ const imagePath = tenInchScreenshots[i];
766
+ if (!existsSync(imagePath)) {
767
+ console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
768
+ continue;
769
+ }
770
+ const imageBuffer = readFileSync(imagePath);
771
+ const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
772
+ await this.uploadImageWithOptionalTimeout(session, language, "tenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
773
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
774
+ result.uploaded.tenInchScreenshots++;
775
+ }
776
+ if (featureGraphic && existsSync(featureGraphic)) {
777
+ const imageBuffer = readFileSync(featureGraphic);
778
+ await this.uploadImageWithOptionalTimeout(session, language, "featureGraphic", imageBuffer, imageUploadTimeoutMs);
779
+ console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
780
+ result.uploaded.featureGraphic = true;
781
+ }
782
+ return result;
783
+ }
784
+ async uploadImageWithOptionalTimeout(session, language, imageType, imageBuffer, timeoutMs) {
785
+ await this.androidPublisher.edits.images.upload({
786
+ auth: session.auth,
787
+ packageName: session.packageName,
788
+ editId: session.editId,
789
+ language,
790
+ imageType,
791
+ media: {
792
+ mimeType: "image/png",
793
+ body: imageBuffer,
794
+ },
795
+ }, timeoutMs ? { timeout: timeoutMs } : undefined);
790
796
  }
791
797
  async getTrack(session, track) {
792
798
  const response = await this.androidPublisher.edits.tracks.get({
@@ -119,6 +119,7 @@ export interface BatchUploadScreenshotsOptions {
119
119
  sevenInchScreenshots?: string[];
120
120
  tenInchScreenshots?: string[];
121
121
  featureGraphic?: string;
122
+ imageUploadTimeoutMs?: number;
122
123
  }
123
124
  /**
124
125
  * Batch Upload Screenshots Result
@@ -5,6 +5,8 @@ interface AsoPushOptions {
5
5
  bundleId?: string;
6
6
  store?: StoreType;
7
7
  uploadImages?: boolean;
8
+ locales?: string[];
9
+ imageUploadTimeoutMs?: number;
8
10
  dryRun?: boolean;
9
11
  }
10
12
  export declare function handleAsoPush(options: AsoPushOptions): Promise<{
@@ -9,7 +9,7 @@ const appResolutionService = new AppResolutionService();
9
9
  const appStoreService = new AppStoreService();
10
10
  const googlePlayService = new GooglePlayService();
11
11
  export async function handleAsoPush(options) {
12
- const { store = "both", uploadImages = false, dryRun = false } = options;
12
+ const { store = "both", uploadImages = false, locales, imageUploadTimeoutMs, dryRun = false, } = options;
13
13
  const resolved = appResolutionService.resolve({
14
14
  slug: options.app,
15
15
  packageName: options.packageName,
@@ -34,6 +34,11 @@ export async function handleAsoPush(options) {
34
34
  if (bundleId)
35
35
  console.error(`[MCP] Bundle ID: ${bundleId}`);
36
36
  console.error(`[MCP] Upload Images: ${uploadImages ? "Yes" : "No"}`);
37
+ if (locales?.length)
38
+ console.error(`[MCP] Locales: ${locales.join(", ")}`);
39
+ if (imageUploadTimeoutMs) {
40
+ console.error(`[MCP] Image Upload Timeout: ${imageUploadTimeoutMs}ms`);
41
+ }
37
42
  console.error(`[MCP] Mode: ${dryRun ? "Dry run" : "Actual push"}`);
38
43
  let config;
39
44
  try {
@@ -136,6 +141,8 @@ export async function handleAsoPush(options) {
136
141
  localAsoData: configData,
137
142
  googlePlayDataPath,
138
143
  uploadImages,
144
+ locales,
145
+ imageUploadTimeoutMs,
139
146
  slug,
140
147
  });
141
148
  results.push(formatPushResult("Google Play", result));
@@ -153,6 +160,8 @@ export async function handleAsoPush(options) {
153
160
  localAsoData: configData,
154
161
  appStoreDataPath,
155
162
  uploadImages,
163
+ locales,
164
+ imageUploadTimeoutMs,
156
165
  slug,
157
166
  });
158
167
  results.push(formatPushResult("App Store", appStoreResult));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-store-api-mcp",
3
- "version": "1.3.10",
3
+ "version": "1.3.13",
4
4
  "description": "MCP server for App Store / Play Store ASO workflows",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",