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`);
              }
            }
          }
        }
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions 'save to local cache' and implies data fetching, but lacks details on permissions needed, rate limits, error handling, cache behavior (e.g., overwrite or merge), or what 'fetch ASO data' entails (e.g., metadata, rankings). This is inadequate for a tool with potential external API calls and local storage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core action ('fetch ASO data') and includes key details (sources and cache saving). There is no wasted verbiage, making it easy for an agent to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of fetching from external stores and saving locally, with no annotations and no output schema, the description is insufficient. It doesn't explain what 'ASO data' includes, the format of saved data, success/error responses, or dependencies on other tools like 'apps-init'. This leaves significant gaps for agent understanding.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, providing clear documentation for all 5 parameters. The description adds no additional parameter semantics beyond implying that 'app', 'packageName', or 'bundleId' identify the target, and 'dryRun' affects saving. This meets the baseline of 3 when schema coverage is high.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('fetch ASO data') and the target resources ('App Store/Google Play'), and mentions saving to local cache. It distinguishes from siblings like 'aso-push' (which likely pushes data) and 'apps-init' (which registers apps). However, it doesn't explicitly differentiate from 'release-pull-notes' or other data-fetching tools, keeping it at 4 instead of 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., needing registered apps via 'apps-init'), exclusions, or comparisons to siblings like 'apps-search' or 'release-pull-notes'. This leaves the agent without context for tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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