pabal-store-api-mcp 1.3.8 → 1.3.9

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=pabal-store-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBhYmFsLW1jcCJdfQ%3D%3D)
4
4
 
5
- [![Pabal Web MCP (English)](https://img.shields.io/badge/Pabal%20Web%20MCP-English-blue)](https://pabal.quartz.best/docs/en-US/pabal-resource-mcp/README) [![Pabal Web MCP (한국어)](https://img.shields.io/badge/Pabal%20Web%20MCP-한국어-green)](https://pabal.quartz.best/docs/ko-KR/pabal-resource-mcp/README)
5
+ [![Pabal Store API MCP (English)](https://img.shields.io/badge/Pabal%20Store%20API%20MCP-English-blue)](https://pabal.quartz.best/en-US/docs/pabal-store-api-mcp/README) [![Pabal Store API MCP (한국어)](https://img.shields.io/badge/Pabal%20Store%20API%20MCP-한국어-green)](https://pabal.quartz.best/ko-KR/docs/pabal-store-api-mcp/README)
6
6
 
7
7
  # MCP server for App Store Connect & Play Console API
8
8
 
@@ -58,8 +58,8 @@ chmod 700 ~/.config/pabal-mcp
58
58
 
59
59
  ## Documentation
60
60
 
61
- 📖 **[Pabal Web MCP Documentation (English)](https://pabal.quartz.best/docs/en-US/pabal-resource-mcp/README)**
62
- 📖 **[Pabal Web MCP 문서 (한국어)](https://pabal.quartz.best/docs/ko-KR/pabal-resource-mcp/README)**
61
+ 📖 **[Pabal Store API MCP Documentation (English)](https://pabal.quartz.best/en-US/docs/pabal-store-api-mcp/README)**
62
+ 📖 **[Pabal Store API MCP 문서 (한국어)](https://pabal.quartz.best/ko-KR/docs/pabal-store-api-mcp/README)**
63
63
 
64
64
  <br>
65
65
 
@@ -280,6 +280,9 @@ export class AppStoreService {
280
280
  locale,
281
281
  screenshots: screenshotsToUpload,
282
282
  });
283
+ if (uploadResult.failed > 0) {
284
+ throw new Error(`Screenshot upload reported ${uploadResult.failed} failed files`);
285
+ }
283
286
  console.error(`[AppStore] ✅ Screenshots for ${locale}: ${uploadResult.uploaded} uploaded, ${uploadResult.deleted} deleted, ${uploadResult.failed} failed`);
284
287
  uploadedLocales.push(locale);
285
288
  }
@@ -297,6 +300,7 @@ export class AppStoreService {
297
300
  }
298
301
  if (failedLocales.length > 0) {
299
302
  console.error(`[AppStore] ❌ Failed: ${failedLocales.join(", ")}`);
303
+ throw new Error(`Screenshot upload failed for locales: ${failedLocales.join(", ")}`);
300
304
  }
301
305
  }
302
306
  try {
@@ -107,6 +107,10 @@ export declare class AppStoreClient {
107
107
  * Delete all screenshots in a screenshot set
108
108
  */
109
109
  private deleteAllScreenshotsInSet;
110
+ private sanitizePathSegment;
111
+ private getScreenshotLockPath;
112
+ private acquireScreenshotUploadLock;
113
+ private preflightScreenshotBatch;
110
114
  /**
111
115
  * Upload multiple screenshots for a locale, replacing existing ones
112
116
  * 1. Find or create screenshot sets for each display type
@@ -4,9 +4,12 @@
4
4
  * Authentication: API Key (Issuer ID, Key ID, Private Key) required
5
5
  * API Documentation: https://developer.apple.com/documentation/appstoreconnectapi
6
6
  */
7
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, } from "node:fs";
8
+ import { basename, dirname, join } from "node:path";
7
9
  import { AppStoreConnectAPI } from "appstore-connect-sdk";
8
10
  import { AppsApi, AppInfosApi, AppInfoLocalizationsApi, AppScreenshotSetsApi, AppScreenshotsApi, AppStoreVersionLocalizationsApi, AppStoreVersionsApi, ResponseError, } from "appstore-connect-sdk/openapi";
9
11
  import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.js";
12
+ import { getAsoDir } from "../../../packages/configs/aso-config/utils.js";
10
13
  import { convertToAsoData, convertToMultilingualAsoData, convertToReleaseNote, fetchScreenshotsForLocalization, mapLocalizationsByLocale, selectEnglishAppName, sortReleaseNotes, sortVersions, } from "./api-converters.js";
11
14
  import { APP_STORE_API_BASE_URL, APP_STORE_PLATFORM, DEFAULT_APP_LIST_LIMIT, DEFAULT_VERSIONS_FETCH_LIMIT, } from "./constants.js";
12
15
  export class AppStoreClient {
@@ -575,8 +578,6 @@ export class AppStoreClient {
575
578
  */
576
579
  async uploadScreenshot(options) {
577
580
  const { imagePath, screenshotDisplayType, locale } = options;
578
- const { readFileSync, statSync } = await import("node:fs");
579
- const { basename } = await import("node:path");
580
581
  try {
581
582
  // Get app and version info
582
583
  const appId = await this.findAppId();
@@ -775,15 +776,75 @@ export class AppStoreClient {
775
776
  const screenshots = screenshotsResponse.data || [];
776
777
  let deletedCount = 0;
777
778
  for (const screenshot of screenshots) {
779
+ await this.deleteScreenshot(screenshot.id);
780
+ deletedCount++;
781
+ }
782
+ return deletedCount;
783
+ }
784
+ sanitizePathSegment(value) {
785
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
786
+ }
787
+ getScreenshotLockPath(locale, displayType) {
788
+ const appIdSegment = this.sanitizePathSegment(this.bundleId);
789
+ const localeSegment = this.sanitizePathSegment(locale);
790
+ const displayTypeSegment = this.sanitizePathSegment(displayType);
791
+ return join(getAsoDir(), "locks", "screenshots", "app-store", appIdSegment, localeSegment, `${displayTypeSegment}.lock`);
792
+ }
793
+ acquireScreenshotUploadLock(lockPath) {
794
+ mkdirSync(dirname(lockPath), { recursive: true });
795
+ let descriptor;
796
+ try {
797
+ descriptor = openSync(lockPath, "wx");
798
+ }
799
+ catch (error) {
800
+ const code = error.code;
801
+ if (code === "EEXIST") {
802
+ throw new Error(`Screenshot upload lock is already held (${lockPath}). Another upload is running for this locale/display type.`);
803
+ }
804
+ throw error;
805
+ }
806
+ closeSync(descriptor);
807
+ return () => {
778
808
  try {
779
- await this.deleteScreenshot(screenshot.id);
780
- deletedCount++;
809
+ unlinkSync(lockPath);
781
810
  }
782
- catch (error) {
783
- console.error(`[AppStore] Warning: Failed to delete screenshot ${screenshot.id}: ${error instanceof Error ? error.message : String(error)}`);
811
+ catch {
812
+ // Ignore lock cleanup failures.
813
+ }
814
+ };
815
+ }
816
+ preflightScreenshotBatch(locale, byDisplayType) {
817
+ for (const [displayType, screenshotList] of byDisplayType) {
818
+ if (screenshotList.length === 0) {
819
+ throw new Error(`Preflight failed for ${locale}/${displayType}: empty screenshot list`);
820
+ }
821
+ const seenPaths = new Set();
822
+ const seenFileNames = new Set();
823
+ for (const screenshot of screenshotList) {
824
+ if (!existsSync(screenshot.path)) {
825
+ throw new Error(`Preflight failed for ${locale}/${displayType}: file not found (${screenshot.path})`);
826
+ }
827
+ const fileStat = statSync(screenshot.path);
828
+ if (!fileStat.isFile()) {
829
+ throw new Error(`Preflight failed for ${locale}/${displayType}: not a file (${screenshot.path})`);
830
+ }
831
+ if (fileStat.size <= 0) {
832
+ throw new Error(`Preflight failed for ${locale}/${displayType}: empty file (${screenshot.path})`);
833
+ }
834
+ if (!screenshot.filename.trim()) {
835
+ throw new Error(`Preflight failed for ${locale}/${displayType}: empty filename (${screenshot.path})`);
836
+ }
837
+ if (seenPaths.has(screenshot.path)) {
838
+ throw new Error(`Preflight failed for ${locale}/${displayType}: duplicate file path (${screenshot.path})`);
839
+ }
840
+ seenPaths.add(screenshot.path);
841
+ const normalizedFileName = screenshot.filename.trim().toLowerCase();
842
+ if (seenFileNames.has(normalizedFileName)) {
843
+ throw new Error(`Preflight failed for ${locale}/${displayType}: duplicate filename (${screenshot.filename})`);
844
+ }
845
+ seenFileNames.add(normalizedFileName);
784
846
  }
785
847
  }
786
- return deletedCount;
787
848
  }
788
849
  /**
789
850
  * Upload multiple screenshots for a locale, replacing existing ones
@@ -793,8 +854,6 @@ export class AppStoreClient {
793
854
  */
794
855
  async uploadScreenshotsForLocale(options) {
795
856
  const { locale, screenshots } = options;
796
- const { readFileSync, statSync, existsSync } = await import("node:fs");
797
- const { basename } = await import("node:path");
798
857
  const result = { uploaded: 0, deleted: 0, failed: 0 };
799
858
  if (screenshots.length === 0) {
800
859
  return result;
@@ -830,45 +889,50 @@ export class AppStoreClient {
830
889
  filename: screenshot.filename,
831
890
  });
832
891
  }
892
+ this.preflightScreenshotBatch(locale, byDisplayType);
833
893
  // Process each display type
834
894
  for (const [displayType, screenshotList] of byDisplayType) {
835
- console.error(`[AppStore] Processing ${displayType} (${screenshotList.length} screenshots)...`);
836
- // Find or create screenshot set
837
- const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, displayType);
838
- // Delete existing screenshots in this set
839
- const deletedCount = await this.deleteAllScreenshotsInSet(screenshotSetId);
840
- if (deletedCount > 0) {
841
- console.error(`[AppStore] 🗑️ Deleted ${deletedCount} existing screenshots`);
842
- result.deleted += deletedCount;
843
- }
844
- // Upload new screenshots in order
845
- for (const screenshot of screenshotList) {
846
- if (!existsSync(screenshot.path)) {
847
- console.error(`[AppStore] ⚠️ File not found: ${screenshot.filename}`);
848
- result.failed++;
849
- continue;
895
+ const releaseLock = this.acquireScreenshotUploadLock(this.getScreenshotLockPath(locale, displayType));
896
+ try {
897
+ console.error(`[AppStore] Processing ${displayType} (${screenshotList.length} screenshots)...`);
898
+ // Find or create screenshot set
899
+ const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, displayType);
900
+ // Delete existing screenshots in this set
901
+ const deletedCount = await this.deleteAllScreenshotsInSet(screenshotSetId);
902
+ if (deletedCount > 0) {
903
+ console.error(`[AppStore] Deleted ${deletedCount} existing screenshots`);
904
+ result.deleted += deletedCount;
850
905
  }
851
- try {
852
- const fileBuffer = readFileSync(screenshot.path);
853
- const fileSize = statSync(screenshot.path).size;
854
- // Create screenshot with upload operation
855
- const screenshotData = await this.createAppScreenshot(screenshotSetId, screenshot.filename, fileSize);
856
- // Upload file
857
- if (screenshotData.uploadOperations &&
858
- screenshotData.uploadOperations.length > 0) {
859
- const uploadOp = screenshotData.uploadOperations[0];
860
- await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method);
906
+ // Upload new screenshots in order
907
+ for (const screenshot of screenshotList) {
908
+ try {
909
+ const fileBuffer = readFileSync(screenshot.path);
910
+ const fileSize = statSync(screenshot.path).size;
911
+ // Create screenshot with upload operation
912
+ const screenshotData = await this.createAppScreenshot(screenshotSetId, screenshot.filename, fileSize);
913
+ // Upload file
914
+ if (screenshotData.uploadOperations &&
915
+ screenshotData.uploadOperations.length > 0) {
916
+ const uploadOp = screenshotData.uploadOperations[0];
917
+ await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method);
918
+ }
919
+ // Commit screenshot
920
+ await this.commitAppScreenshot(screenshotData.id);
921
+ console.error(`[AppStore] ✅ ${screenshot.filename}`);
922
+ result.uploaded++;
923
+ }
924
+ catch (error) {
925
+ result.failed++;
926
+ throw new Error(`[${displayType}] ${screenshot.filename}: ${error instanceof Error ? error.message : String(error)}`);
861
927
  }
862
- // Commit screenshot
863
- await this.commitAppScreenshot(screenshotData.id);
864
- console.error(`[AppStore] ✅ ${screenshot.filename}`);
865
- result.uploaded++;
866
- }
867
- catch (error) {
868
- console.error(`[AppStore] ❌ ${screenshot.filename}: ${error instanceof Error ? error.message : String(error)}`);
869
- result.failed++;
870
928
  }
871
929
  }
930
+ finally {
931
+ releaseLock();
932
+ }
933
+ }
934
+ if (result.failed > 0) {
935
+ throw new Error(`Screenshot upload completed with ${result.failed} failed files`);
872
936
  }
873
937
  return result;
874
938
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-store-api-mcp",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
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",