Skip to main content
Glama

apps-add

Register apps by bundle ID or package name to manage App Store Connect and Google Play Console metadata. Automatically checks both stores for app registration.

Instructions

Register individual app by bundleId or packageName. Automatically checks both stores.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
identifierYesApp identifier (bundleId or packageName, e.g., com.example.app)
slugNoCustom slug (if not specified, uses last part of identifier)
storeNoStore to check (default: both)

Implementation Reference

  • Main handler function for 'apps-add' tool. Registers or updates an app by bundle ID or package name. Fetches app info and supported locales from App Store and/or Google Play, generates slug, checks for duplicates, and saves to registered apps config.
    export async function handleAddApp(options: AddAppOptions) {
      const { identifier, slug: customSlug, store = "both" } = options;
    
      console.error(`[MCP] πŸ“± Adding app: ${identifier} (store: ${store})`);
    
      if (!identifier) {
        return {
          content: [
            {
              type: "text" as const,
              text: `❌ identifier is required.
    
    Usage:
    \`\`\`json
    { "identifier": "com.example.app" }
    { "identifier": "com.example.app", "slug": "myapp" }
    { "identifier": "com.example.app", "store": "googlePlay" }
    \`\`\``,
            },
          ],
        };
      }
    
      // Check if already registered
      let existing;
      try {
        existing = findApp(identifier);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        return {
          content: [
            {
              type: "text" as const,
              text: `❌ Failed to load registered apps: ${message}`,
            },
          ],
        };
      }
      if (existing) {
        // Update language info for existing apps
        let appsConfig;
        try {
          appsConfig = loadRegisteredApps();
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          return {
            content: [
              {
                type: "text" as const,
                text: `❌ Failed to load registered apps: ${message}`,
              },
            ],
          };
        }
        const appIndex = appsConfig.apps.findIndex((a) => a.slug === existing.slug);
    
        if (appIndex >= 0) {
          let updated = false;
          const updateResults: string[] = [];
    
          // Update App Store language info
          if (store === "both" || store === "appStore") {
            if (existing.appStore) {
              const asResult = await appStoreService.fetchAppInfo(identifier);
    
              if (asResult.found && asResult.supportedLocales) {
                if (!appsConfig.apps[appIndex].appStore) {
                  appsConfig.apps[appIndex].appStore = {
                    bundleId: identifier,
                    appId: asResult.appId,
                    name: asResult.name,
                  };
                }
                appsConfig.apps[appIndex].appStore!.supportedLocales =
                  asResult.supportedLocales;
                updated = true;
                updateResults.push(
                  `🍎 App Store: Updated locales (${asResult.supportedLocales.length})`
                );
              }
            }
          }
    
          // Update Google Play language info
          if (store === "both" || store === "googlePlay") {
            if (existing.googlePlay || store === "googlePlay") {
              const gpResult = await googlePlayService.fetchAppInfo(identifier);
    
              if (gpResult.found && gpResult.supportedLocales) {
                if (!appsConfig.apps[appIndex].googlePlay) {
                  appsConfig.apps[appIndex].googlePlay = {
                    packageName: identifier,
                    name: gpResult.name,
                  };
                }
                appsConfig.apps[appIndex].googlePlay!.supportedLocales =
                  gpResult.supportedLocales;
                if (gpResult.name) {
                  appsConfig.apps[appIndex].googlePlay!.name = gpResult.name;
                }
                updated = true;
                updateResults.push(
                  `πŸ€– Google Play: Updated locales (${gpResult.supportedLocales.length})`
                );
              }
            }
          }
    
          if (updated) {
            saveRegisteredApps(appsConfig);
            const updatedApp = appsConfig.apps[appIndex];
    
            const localeInfo: string[] = [];
            if (
              updatedApp.appStore?.supportedLocales &&
              updatedApp.appStore.supportedLocales.length > 0
            ) {
              localeInfo.push(
                `β€’ App Store locales: ${updatedApp.appStore.supportedLocales.join(", ")}`
              );
            }
            if (
              updatedApp.googlePlay?.supportedLocales &&
              updatedApp.googlePlay.supportedLocales.length > 0
            ) {
              localeInfo.push(
                `β€’ Google Play locales: ${updatedApp.googlePlay.supportedLocales.join(", ")}`
              );
            }
    
            return {
              content: [
                {
                  type: "text" as const,
                  text: `βœ… App language info updated
    
    β€’ Slug: \`${updatedApp.slug}\`
    β€’ Name: ${updatedApp.name}
    β€’ App Store: ${updatedApp.appStore ? `βœ… ${updatedApp.appStore.bundleId}` : "❌"}
    β€’ Google Play: ${updatedApp.googlePlay ? `βœ… ${updatedApp.googlePlay.packageName}` : "❌"}
    ${updateResults.length > 0 ? `\n**Updates:**\n${updateResults.map((r) => `  β€’ ${r}`).join("\n")}` : ""}
    ${localeInfo.length > 0 ? `\n**Supported Languages:**\n${localeInfo.map((l) => `  ${l}`).join("\n")}` : ""}`,
                },
              ],
              _meta: { app: updatedApp },
            };
          }
        }
    
        // If no updates were made, return existing info
        const localeInfo: string[] = [];
        if (
          existing.appStore?.supportedLocales &&
          existing.appStore.supportedLocales.length > 0
        ) {
          localeInfo.push(
            `β€’ App Store locales: ${existing.appStore.supportedLocales.join(", ")}`
          );
        }
        if (
          existing.googlePlay?.supportedLocales &&
          existing.googlePlay.supportedLocales.length > 0
        ) {
          localeInfo.push(
            `β€’ Google Play locales: ${existing.googlePlay.supportedLocales.join(", ")}`
          );
        }
    
        return {
          content: [
            {
              type: "text" as const,
              text: `⏭️ App is already registered.
    
    β€’ Slug: \`${existing.slug}\`
    β€’ Name: ${existing.name}
    β€’ App Store: ${existing.appStore ? `βœ… ${existing.appStore.bundleId}` : "❌"}
    β€’ Google Play: ${existing.googlePlay ? `βœ… ${existing.googlePlay.packageName}` : "❌"}
    ${localeInfo.length > 0 ? `\n**Supported Languages:**\n${localeInfo.map((l) => `  ${l}`).join("\n")}` : ""}`,
            },
          ],
          _meta: { app: existing },
        };
      }
    
      const slug = customSlug || generateSlug(identifier);
    
      // Check for slug duplicates
      const slugExists = findApp(slug);
      if (slugExists) {
        return {
          content: [
            {
              type: "text" as const,
              text: `❌ slug "${slug}" is already in use. Please specify a different slug.
    
    \`\`\`json
    { "identifier": "${identifier}", "slug": "different-slug" }
    \`\`\``,
            },
          ],
        };
      }
    
      // Fetch app information by store (μ–Έμ–΄ 정보 포함)
      let appStoreInfo: RegisteredApp["appStore"] = undefined;
      let googlePlayInfo: RegisteredApp["googlePlay"] = undefined;
      let appName = identifier;
    
      const results: string[] = [];
    
      // Check App Store
      if (store === "both" || store === "appStore") {
        console.error(`[MCP]   πŸ” Searching App Store for: ${identifier}`);
        const asResult = await appStoreService.fetchAppInfo(identifier);
        if (asResult.found) {
          appStoreInfo = toRegisteredAppStoreInfo({
            bundleId: identifier,
            appInfo: asResult,
          });
          appName = asResult.name || appName;
          const localeInfo =
            asResult.supportedLocales && asResult.supportedLocales.length > 0
              ? ` (${asResult.supportedLocales.length} locales)`
              : "";
          results.push(`🍎 App Store: βœ… Found (${asResult.name})${localeInfo}`);
        } else {
          results.push(`🍎 App Store: ❌ Not found`);
        }
      }
    
      // Check Google Play
      if (store === "both" || store === "googlePlay") {
        console.error(`[MCP]   πŸ” Searching Google Play for: ${identifier}`);
        const gpResult = await googlePlayService.fetchAppInfo(identifier);
        if (gpResult.found) {
          googlePlayInfo = toRegisteredGooglePlayInfo({
            packageName: identifier,
            appInfo: gpResult,
          });
          appName = gpResult.name || appName;
          const localeInfo =
            gpResult.supportedLocales && gpResult.supportedLocales.length > 0
              ? ` (${gpResult.supportedLocales.length} locales)`
              : "";
          results.push(`πŸ€– Google Play: βœ… Found (${gpResult.name})${localeInfo}`);
        } else {
          results.push(`πŸ€– Google Play: ❌ Not found`);
        }
      }
    
      // Must be found in at least one store
      if (!appStoreInfo && !googlePlayInfo) {
        return {
          content: [
            {
              type: "text" as const,
              text: `❌ App not found.
    
    **Search Results:**
    ${results.map((r) => `  β€’ ${r}`).join("\n")}
    
    **Things to Check:**
    β€’ Verify identifier is correct: \`${identifier}\`
    β€’ Verify app is registered in the store
    β€’ Verify authentication settings are correct (use auth-check tool)`,
            },
          ],
        };
      }
    
      // Register app
      try {
        console.error(`[MCP]   πŸ’Ύ Registering app with slug: ${slug}`);
        const newApp = registerApp({
          slug,
          name: appName,
          appStore: appStoreInfo,
          googlePlay: googlePlayInfo,
        });
        console.error(`[MCP]   βœ… App registered successfully`);
    
        const storeIcons = [
          appStoreInfo ? "🍎" : null,
          googlePlayInfo ? "πŸ€–" : null,
        ]
          .filter(Boolean)
          .join("+");
    
        const localeInfo: string[] = [];
        if (
          appStoreInfo?.supportedLocales &&
          appStoreInfo.supportedLocales.length > 0
        ) {
          localeInfo.push(
            `β€’ App Store locales: ${appStoreInfo.supportedLocales.join(", ")}`
          );
        }
        if (
          googlePlayInfo?.supportedLocales &&
          googlePlayInfo.supportedLocales.length > 0
        ) {
          localeInfo.push(
            `β€’ Google Play locales: ${googlePlayInfo.supportedLocales.join(", ")}`
          );
        }
    
        return {
          content: [
            {
              type: "text" as const,
              text: `βœ… App registration complete (${storeIcons})
    
    **Registration Info:**
    β€’ Slug: \`${newApp.slug}\`
    β€’ Name: ${newApp.name}
    ${appStoreInfo ? `β€’ App Store: ${appStoreInfo.bundleId} (ID: ${appStoreInfo.appId})` : ""}
    ${googlePlayInfo ? `β€’ Google Play: ${googlePlayInfo.packageName}` : ""}
    ${localeInfo.length > 0 ? `\n**Supported Languages:**\n${localeInfo.map((l) => `  ${l}`).join("\n")}` : ""}
    
    **Search Results:**
    ${results.map((r) => `  β€’ ${r}`).join("\n")}
    
    You can now reference this app in other tools using the \`app: "${slug}"\` parameter.`,
            },
          ],
          _meta: { app: newApp },
        };
      } catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        return {
          content: [
            {
              type: "text" as const,
              text: `❌ App registration failed: ${msg}`,
            },
          ],
        };
      }
    }
  • src/index.ts:184-206 (registration)
    Registration of the 'apps-add' tool using registerToolWithInfo, including description, input schema, and linking to the handleAddApp handler.
    registerToolWithInfo(
      "apps-add",
      {
        description:
          "Register individual app by bundleId or packageName. Automatically checks both stores.",
        inputSchema: z.object({
          identifier: z
            .string()
            .describe(
              "App identifier (bundleId or packageName, e.g., com.example.app)"
            ),
          slug: z
            .string()
            .optional()
            .describe(
              "Custom slug (if not specified, uses last part of identifier)"
            ),
          store: storeSchema.describe("Store to check (default: both)"),
        }),
      },
      handleAddApp,
      "App Management"
    );
  • Zod input schema definition for the 'apps-add' tool parameters: identifier (required), slug (optional), store (optional).
    inputSchema: z.object({
      identifier: z
        .string()
        .describe(
          "App identifier (bundleId or packageName, e.g., com.example.app)"
        ),
      slug: z
        .string()
        .optional()
        .describe(
          "Custom slug (if not specified, uses last part of identifier)"
        ),
      store: storeSchema.describe("Store to check (default: both)"),
    }),
  • TypeScript interface defining the input options for the handleAddApp function.
    interface AddAppOptions {
      /** App identifier (bundleId or packageName) */
      identifier: string;
      /** Custom slug (if not specified, uses last part of identifier) */
      slug?: string;
      /** Target store (default: both - check both stores) */
      store?: "appStore" | "googlePlay" | "both";
    }
Behavior2/5

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

No annotations are provided, so the description carries full burden. It mentions 'Register' (implying a write operation) and 'checks both stores', but doesn't disclose behavioral traits such as permissions required, whether registration is idempotent, rate limits, or what happens on failure. This is inadequate for a mutation tool with zero annotation coverage.

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 with zero waste. It's front-loaded with the core purpose and includes essential details without redundancy, making it highly concise and well-structured.

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 this is a mutation tool ('Register') with no annotations, no output schema, and 3 parameters, the description is incomplete. It lacks crucial details like what the tool returns, error conditions, or side effects, leaving significant gaps for an AI agent to understand its full behavior.

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%, so the schema fully documents all parameters. The description adds minimal value beyond the schema, mentioning 'bundleId or packageName' (implied by the schema's description for 'identifier') and 'checks both stores' (implied by the 'store' parameter's enum). Baseline 3 is appropriate as the schema does the heavy lifting.

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 ('Register') and resource ('individual app'), specifying it works by 'bundleId or packageName' and 'checks both stores'. However, it doesn't explicitly differentiate from sibling tools like 'apps-init' or 'apps-search', which might have overlapping functionality.

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 like 'apps-init' (which might initialize multiple apps) or 'apps-search' (which might search for apps). It mentions 'Automatically checks both stores' but doesn't clarify if this is unique to this tool or a general feature.

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