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

TableJSON 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

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