Skip to main content
Glama

aso-pull

Fetch ASO data from App Store and Google Play to save locally for analysis, supporting both stores with configurable options.

Instructions

Fetch ASO data from App Store/Google Play and save to local cache.

Input Schema

NameRequiredDescriptionDefault
appNoRegistered app slug (app registered via apps-init)
packageNameNoGoogle Play package name
bundleIdNoApp Store bundle ID
storeNoTarget store (default: both)
dryRunNoIf true, only outputs result without actually saving

Input Schema (JSON Schema)

{ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "app": { "description": "Registered app slug (app registered via apps-init)", "type": "string" }, "bundleId": { "description": "App Store bundle ID", "type": "string" }, "dryRun": { "description": "If true, only outputs result without actually saving", "type": "boolean" }, "packageName": { "description": "Google Play package name", "type": "string" }, "store": { "description": "Target store (default: both)", "enum": [ "appStore", "googlePlay", "both" ], "type": "string" } }, "type": "object" }

Implementation Reference

  • Main handler function that resolves the app, fetches ASO data from App Store/Google Play using services, optionally downloads screenshots, updates registered apps locales, and saves to local cache or returns dry-run preview.
    export async function handleAsoPull(options: AsoPullOptions) { const { app, store, dryRun = false } = options; let { packageName, bundleId } = options; const { store: targetStore, includeAppStore, includeGooglePlay, } = getStoreTargets(store); const resolved = appResolutionService.resolve({ slug: app, packageName, bundleId, }); if (!resolved.success) { return { content: [ { type: "text" as const, text: resolved.error.message, }, ], }; } const { slug, bundleId: resolvedBundleId, packageName: resolvedPackageName, hasAppStore, hasGooglePlay, } = resolved.data; bundleId = resolvedBundleId; packageName = resolvedPackageName; console.error(`[MCP] πŸ“₯ Pulling ASO data`); console.error(`[MCP] Store: ${targetStore}`); console.error(`[MCP] App: ${slug}`); if (packageName) console.error(`[MCP] Package Name: ${packageName}`); if (bundleId) console.error(`[MCP] Bundle ID: ${bundleId}`); console.error(`[MCP] Mode: ${dryRun ? "Dry run" : "Actual fetch"}`); let config; try { config = loadConfig(); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: "text" as const, text: `❌ Failed to load config: ${message}` }, ], isError: true, }; } const syncedData: AsoData = {}; const pullDir = getAsoPullDir(); if (includeGooglePlay) { if (!hasGooglePlay) { console.error( `[MCP] ⏭️ Skipping Google Play (not registered for Google Play)` ); } else if (!config.playStore) { console.error( `[MCP] ⏭️ Skipping Google Play (not configured in ~/.config/pabal-mcp/config.json)` ); } else if (!packageName) { console.error( `[MCP] ⏭️ Skipping Google Play (no packageName provided)` ); } else { const clientResult = googlePlayService.createClient(packageName); if (!clientResult.success) { console.error( `[MCP] ❌ Failed to create Google Play client: ${clientResult.error.message}` ); } else { try { console.error(`[MCP] πŸ“₯ Fetching from Google Play...`); const data = await clientResult.data.pullAllLanguagesAsoData(); syncedData.googlePlay = data; console.error(`[MCP] βœ… Google Play data fetched`); // Update registered-apps.json with pulled locales if (data.locales && Object.keys(data.locales).length > 0) { const locales = Object.keys(data.locales); try { const updated = updateAppSupportedLocales({ identifier: packageName, store: "googlePlay", locales, }); if (updated) { console.error( `[MCP] βœ… Updated registered-apps.json with ${locales.length} Google Play locales` ); } } catch (updateError) { console.error( `[MCP] ⚠️ Failed to update registered-apps.json: ${ updateError instanceof Error ? updateError.message : String(updateError) }` ); } } } catch (error) { console.error(`[MCP] ❌ Google Play fetch failed:`, error); } } } } if (includeAppStore) { if (!hasAppStore) { console.error( `[MCP] ⏭️ Skipping App Store (not registered for App Store)` ); } else if (!config.appStore) { console.error( `[MCP] ⏭️ Skipping App Store (not configured in ~/.config/pabal-mcp/config.json)` ); } else if (!bundleId) { console.error(`[MCP] ⏭️ Skipping App Store (no bundleId provided)`); } else { const clientResult = appStoreService.createClient(bundleId); if (!clientResult.success) { console.error( `[MCP] ❌ Failed to create App Store client: ${clientResult.error.message}` ); } else { try { console.error(`[MCP] πŸ“₯ Fetching from App Store...`); const data = await clientResult.data.pullAllLocalesAsoData(); syncedData.appStore = data; console.error(`[MCP] βœ… App Store data fetched`); // Update registered-apps.json with pulled locales if (data.locales && Object.keys(data.locales).length > 0) { const locales = Object.keys(data.locales); try { const updated = updateAppSupportedLocales({ identifier: bundleId, store: "appStore", locales, }); if (updated) { console.error( `[MCP] βœ… Updated registered-apps.json with ${locales.length} App Store locales` ); } } catch (updateError) { console.error( `[MCP] ⚠️ Failed to update registered-apps.json: ${ updateError instanceof Error ? updateError.message : String(updateError) }` ); } } } catch (error) { console.error(`[MCP] ❌ App Store fetch failed:`, error); } } } } if (dryRun) { return { content: [ { type: "text" as const, text: `πŸ“‹ Dry run - Data that would be saved:\n${JSON.stringify( syncedData, null, 2 )}`, }, ], }; } saveAsoData(slug, syncedData, { asoDir: pullDir }); await downloadScreenshotsToAso(slug, syncedData, pullDir); return { content: [ { type: "text" as const, text: `βœ… ASO data pulled\n` + ` Google Play: ${syncedData.googlePlay ? "βœ“" : "βœ—"}\n` + ` App Store: ${syncedData.appStore ? "βœ“" : "βœ—"}`, }, ], }; }
  • src/index.ts:234-255 (registration)
    Tool registration call that associates the 'aso-pull' name with its description, Zod input schema, the handleAsoPull handler function, and category.
    registerToolWithInfo( "aso-pull", { description: "Fetch ASO data from App Store/Google Play and save to local cache.", inputSchema: z.object({ app: z .string() .optional() .describe("Registered app slug (app registered via apps-init)"), packageName: z.string().optional().describe("Google Play package name"), bundleId: z.string().optional().describe("App Store bundle ID"), store: storeSchema.describe("Target store (default: both)"), dryRun: z .boolean() .optional() .describe("If true, only outputs result without actually saving"), }), }, handleAsoPull, "ASO Data Sync" );
  • Zod schema defining the input parameters for the aso-pull tool, including optional app slug, packageName, bundleId, store filter, and dryRun flag.
    inputSchema: z.object({ app: z .string() .optional() .describe("Registered app slug (app registered via apps-init)"), packageName: z.string().optional().describe("Google Play package name"), bundleId: z.string().optional().describe("App Store bundle ID"), store: storeSchema.describe("Target store (default: both)"), dryRun: z .boolean() .optional() .describe("If true, only outputs result without actually saving"), }),
  • TypeScript type interface for the handler input options, matching the Zod schema.
    interface AsoPullOptions { app?: string; // Registered app slug packageName?: string; // For Google Play bundleId?: string; // For App Store store?: StoreType; dryRun?: boolean; }
  • Helper function called by the handler to download screenshots from fetched ASO data to local ASO directories for both stores.
    async function downloadScreenshotsToAso( slug: string, asoData: AsoData, asoDir: string ): Promise<void> { const productStoreRoot = getPullProductAsoDir(slug, asoDir); if (asoData.googlePlay) { const googlePlayData = isGooglePlayMultilingual(asoData.googlePlay) ? asoData.googlePlay : convertToMultilingual( asoData.googlePlay, asoData.googlePlay.defaultLanguage ); const languages = Object.keys(googlePlayData.locales); const defaultLanguage = googlePlayData.defaultLocale; const targetLanguage = (defaultLanguage && googlePlayData.locales[defaultLanguage] ? defaultLanguage : languages[0]) || null; if (targetLanguage) { const localeData = googlePlayData.locales[targetLanguage]; const screenshotDir = getScreenshotDir( productStoreRoot, "google-play", targetLanguage ); if (localeData.screenshots.phone?.length > 0) { console.error( `[MCP] πŸ“₯ Downloading ${localeData.screenshots.phone.length} Google Play phone screenshots...` ); for (let i = 0; i < localeData.screenshots.phone.length; i++) { const url = localeData.screenshots.phone[i]; const outputPath = getScreenshotFilePath( screenshotDir, `phone-${i + 1}.png` ); try { if (isLocalAssetPath(url)) { copyLocalAssetToAso(url, outputPath); } else { await downloadImage(url, outputPath); } console.error(`[MCP] βœ… phone-${i + 1}.png`); } catch (error) { console.error( `[MCP] ❌ Failed to handle screenshot ${url}: ${ error instanceof Error ? error.message : String(error) }` ); } } } if (localeData.featureGraphic) { console.error(`[MCP] πŸ“₯ Downloading Feature Graphic...`); const outputPath = getScreenshotFilePath( screenshotDir, "feature-graphic.png" ); try { if (isLocalAssetPath(localeData.featureGraphic)) { copyLocalAssetToAso(localeData.featureGraphic, outputPath); } else { await downloadImage(localeData.featureGraphic, outputPath); } console.error(`[MCP] βœ… feature-graphic.png`); } catch (error) { console.error( `[MCP] ❌ Failed to handle feature graphic ${localeData.featureGraphic}: ${ error instanceof Error ? error.message : String(error) }` ); } } } } if (asoData.appStore) { const appStoreData = isAppStoreMultilingual(asoData.appStore) ? asoData.appStore : convertToMultilingual(asoData.appStore, asoData.appStore.locale); const locales = Object.keys(appStoreData.locales); const defaultLocale = appStoreData.defaultLocale; const targetLocale = (defaultLocale && appStoreData.locales[defaultLocale] ? defaultLocale : locales[0]) || null; if (targetLocale) { const localeData = appStoreData.locales[targetLocale]; const screenshotDir = getScreenshotDir( productStoreRoot, "app-store", targetLocale ); const screenshotTypes = ["iphone65", "iphone61", "ipadPro129"] as const; for (const type of screenshotTypes) { const screenshots = localeData.screenshots[type]; if (screenshots && screenshots.length > 0) { console.error( `[MCP] πŸ“₯ Downloading ${screenshots.length} App Store ${type} screenshots...` ); for (let i = 0; i < screenshots.length; i++) { let url = screenshots[i]; const outputPath = getScreenshotFilePath( screenshotDir, `${type}-${i + 1}.png` ); if (isLocalAssetPath(url)) { copyLocalAssetToAso(url, outputPath); } else { if (url.includes("{w}") || url.includes("{h}")) { url = resolveAppStoreImageUrl(url); } try { await downloadImage(url, outputPath); } catch (error) { console.error( `[MCP] ❌ Failed to handle screenshot ${url}: ${ error instanceof Error ? error.message : String(error) }` ); } } console.error(`[MCP] βœ… ${type}-${i + 1}.png`); } } } } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quartz-labs-dev/pabal-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server