pabal-store-api-mcp 1.3.7 → 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
|
[](https://cursor.com/en/install-mcp?name=pabal-store-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBhYmFsLW1jcCJdfQ%3D%3D)
|
|
4
4
|
|
|
5
|
-
[](https://pabal.quartz.best/en-US/docs/pabal-store-api-mcp/README) [](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
|
|
62
|
-
📖 **[Pabal
|
|
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 {
|
package/dist/src/index.js
CHANGED
|
@@ -12,6 +12,17 @@ import { handleAsoCreateVersion } from "./tools/release/create.js";
|
|
|
12
12
|
import { handleAsoPullReleaseNotes } from "./tools/release/pull-notes.js";
|
|
13
13
|
import { handleUpdateNotes } from "./tools/release/update-notes.js";
|
|
14
14
|
import { handleCheckLatestVersions } from "./tools/release/check-versions.js";
|
|
15
|
+
// Redirect console.log and console.info to stderr
|
|
16
|
+
// This prevents third-party libraries (like appstore-connect-sdk) from corrupting
|
|
17
|
+
// the stdout JSON-RPC stream required by strict MCP clients (like Antigravity).
|
|
18
|
+
const originalLog = console.log;
|
|
19
|
+
const originalInfo = console.info;
|
|
20
|
+
console.log = function (...args) {
|
|
21
|
+
console.error(...args);
|
|
22
|
+
};
|
|
23
|
+
console.info = function (...args) {
|
|
24
|
+
console.error(...args);
|
|
25
|
+
};
|
|
15
26
|
// MCP config sets cwd to project root, so we don't need to chdir
|
|
16
27
|
// Just verify we're in the right place
|
|
17
28
|
console.error(`[MCP] 📂 Working directory: ${process.cwd()}`);
|
|
@@ -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
|
-
|
|
780
|
-
deletedCount++;
|
|
809
|
+
unlinkSync(lockPath);
|
|
781
810
|
}
|
|
782
|
-
catch
|
|
783
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
}
|