Skip to main content
Glama

release-update-notes

Update release notes for App Store and Google Play versions. Manage 'What's New' content across stores and locales to inform users about app updates.

Instructions

Update release notes (What's New) for App Store/Google Play version.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
appNoRegistered app slug
packageNameNoGoogle Play package name
bundleIdNoApp Store bundle ID
storeNoTarget store (default: both)
versionIdNoApp Store version ID (auto-detects editable version if not specified)
whatsNewNoRelease notes by locale (e.g., { "en-US": "Bug fixes", "ko": "Bug fixes" })
textNoSource text to translate to all supported languages
sourceLocaleNoSource locale (default: en-US)

Implementation Reference

  • The main handler function `handleUpdateNotes` that implements the tool logic. It resolves the app, handles translation requests if `whatsNew` or `text` is incomplete, collects supported locales, separates translations by store, and updates release notes via AppStoreService and GooglePlayService.
    export async function handleUpdateNotes(options: UpdateNotesOptions) {
      const {
        app,
        versionId,
        whatsNew,
        text,
        sourceLocale = "en-US",
        store = "both",
      } = options;
      let { bundleId, packageName } = options;
    
      const { loadConfig } =
        await import("@/packages/configs/secrets-config/config");
    
      const resolved = appResolutionService.resolve({
        slug: app,
        bundleId,
        packageName,
      });
    
      if (!resolved.success) {
        return {
          content: [
            {
              type: "text" as const,
              text: resolved.error.message,
            },
          ],
        };
      }
    
      const {
        slug,
        bundleId: resolvedBundleId,
        packageName: resolvedPackageName,
        hasAppStore,
        hasGooglePlay,
        app: registeredApp,
      } = resolved.data;
    
      bundleId = resolvedBundleId;
      packageName = resolvedPackageName;
    
      const includeAppStore = store === "both" || store === "appStore";
      const includeGooglePlay = store === "both" || store === "googlePlay";
    
      // Determine what to update
      let finalWhatsNew: Record<string, string> = {};
    
      if (whatsNew && Object.keys(whatsNew).length > 0) {
        // Step 1: Get supported locales for the app
        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,
          };
        }
        let appStoreLocales: string[] = [];
        let googlePlayLocales: string[] = [];
    
        // Check if locales are already in registered app
        const {
          appStore: existingAppStoreLocales,
          googlePlay: existingGooglePlayLocales,
        } = collectSupportedLocales({ app: registeredApp, store });
    
        // If locales are missing, fetch from API
        if (includeAppStore && hasAppStore && bundleId) {
          if (existingAppStoreLocales.length > 0) {
            appStoreLocales = existingAppStoreLocales;
          } else if (config.appStore) {
            // Fetch from App Store API
            const appInfo = await appStoreService.fetchAppInfo(bundleId);
    
            if (appInfo.found) {
              if (appInfo.supportedLocales) {
                appStoreLocales = appInfo.supportedLocales;
                // Update registered app with fetched locales
                if (registeredApp.appStore) {
                  registeredApp.appStore.supportedLocales =
                    appInfo.supportedLocales;
                }
              }
            } else if (appInfo.error) {
              console.error(
                `[MCP]   ⚠️ Failed to fetch App Store locales: ${appInfo.error.message}`
              );
            }
          }
        }
    
        if (includeGooglePlay && hasGooglePlay && packageName) {
          if (existingGooglePlayLocales.length > 0) {
            googlePlayLocales = existingGooglePlayLocales;
          } else if (config.playStore?.serviceAccountJson) {
            // Fetch from Google Play API
            const appInfo = await googlePlayService.fetchAppInfo(packageName);
    
            if (appInfo.found) {
              if (appInfo.supportedLocales) {
                googlePlayLocales = appInfo.supportedLocales;
                // Update registered app with fetched locales
                if (registeredApp.googlePlay) {
                  registeredApp.googlePlay.supportedLocales =
                    appInfo.supportedLocales;
                }
              }
            } else if (appInfo.error) {
              console.error(
                `[MCP]   ⚠️ Failed to fetch Google Play locales: ${appInfo.error.message}`
              );
            }
          }
        }
    
        // Collect all unique supported locales
        const allSupportedLocales = new Set<string>();
        if (appStoreLocales.length > 0) {
          appStoreLocales.forEach((locale) => allSupportedLocales.add(locale));
        }
        if (googlePlayLocales.length > 0) {
          googlePlayLocales.forEach((locale) => allSupportedLocales.add(locale));
        }
    
        // Step 2: Check if all supported locales are provided
        const providedLocales = Object.keys(whatsNew);
        const missingLocales = Array.from(allSupportedLocales).filter(
          (locale) => !providedLocales.includes(locale)
        );
    
        // Step 3: Detect the language of provided whatsNew
        const providedText = Object.values(whatsNew)[0]; // Get first provided text
        const detectedLocale = providedLocales[0]; // Use first provided locale as detected locale
    
        // If not all supported locales are provided, request translation
        if (missingLocales.length > 0 && providedText) {
          // Step 3a: If detected locale is not sourceLocale, translate to sourceLocale first
          if (detectedLocale !== sourceLocale) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: `🌐 Translation Pipeline Required
    
    **Step 1: Translate to Default Locale**
    
    **Detected Locale**: ${detectedLocale}
    **Default Locale** (sourceLocale): ${sourceLocale}
    
    **Text to translate** (${detectedLocale}):
    ${providedText}
    
    **Instructions**:
    1. First, translate the text from "${detectedLocale}" to "${sourceLocale}" (default locale)
    2. Then, translate the ${sourceLocale} text to all missing supported locales
    3. Call this function again with the \`whatsNew\` parameter containing all translations
    
    **App Store Supported Locales** (${appStoreLocales.length}):
    ${appStoreLocales.length > 0 ? appStoreLocales.join(", ") : "N/A"}
    
    **Google Play Supported Locales** (${googlePlayLocales.length}):
    ${googlePlayLocales.length > 0 ? googlePlayLocales.join(", ") : "N/A"}
    
    **Provided Locales** (${providedLocales.length}):
    ${providedLocales.join(", ")}
    
    **Missing Locales** (${missingLocales.length}):
    ${missingLocales.join(", ")}
    
    **All Required Locales** (${Array.from(allSupportedLocales).length}):
    ${Array.from(allSupportedLocales).join(", ")}
    
    Example:
    \`\`\`json
    {
      "app": "${slug}",
      "store": "${store}",
      "whatsNew": {
        "${detectedLocale}": "${providedText}",
        "${sourceLocale}": "Translated to default locale",
        "${missingLocales.join(`": "Translation", "`)}": "Translation",
        ...
      }
    }
    \`\`\`
    
    Note: Provide translations for ALL supported locales including the ones already provided.`,
                },
              ],
              _meta: {
                translationPipeline: {
                  step: 1,
                  detectedLocale,
                  sourceLocale,
                  providedText,
                  providedLocales,
                  missingLocales: Array.from(missingLocales),
                  allSupportedLocales: Array.from(allSupportedLocales),
                  appStoreLocales,
                  googlePlayLocales,
                },
                registeredApp,
                slug,
                store,
                versionId,
              },
            };
          }
    
          // Step 3b: If sourceLocale is already provided or detected, translate to all missing locales
          const hasSourceLocale =
            providedLocales.includes(sourceLocale) ||
            detectedLocale === sourceLocale;
          const sourceText = whatsNew[sourceLocale] || providedText;
    
          if (hasSourceLocale) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: `🌐 Translation Required
    
    **Step 2: Translate Default Locale to All Supported Locales**
    
    **Source Text** (${sourceLocale}):
    ${sourceText}
    
    **App Store Supported Locales** (${appStoreLocales.length}):
    ${appStoreLocales.length > 0 ? appStoreLocales.join(", ") : "N/A"}
    
    **Google Play Supported Locales** (${googlePlayLocales.length}):
    ${googlePlayLocales.length > 0 ? googlePlayLocales.join(", ") : "N/A"}
    
    **Already Provided Locales** (${providedLocales.length}):
    ${providedLocales.join(", ")}
    
    **Missing Locales to Translate** (${missingLocales.length}):
    ${missingLocales.join(", ")}
    
    **Instructions**:
    Translate the ${sourceLocale} text to all missing supported locales and call this function again with the \`whatsNew\` parameter containing ALL translations (including the ones already provided).
    
    Example:
    \`\`\`json
    {
      "app": "${slug}",
      "store": "${store}",
      "whatsNew": {
        "${providedLocales.join(`": "${whatsNew[providedLocales[0]]}", "`)}": "${whatsNew[providedLocales[0]]}",
        "${sourceLocale}": "${sourceText}",
        "${missingLocales.join(`": "Translation", "`)}": "Translation",
        ...
      }
    }
    \`\`\`
    
    Note: Provide translations for ALL supported locales. Include the already provided translations as well.`,
                },
              ],
              _meta: {
                translationPipeline: {
                  step: 2,
                  sourceLocale,
                  sourceText,
                  providedLocales,
                  missingLocales: Array.from(missingLocales),
                  allSupportedLocales: Array.from(allSupportedLocales),
                  appStoreLocales,
                  googlePlayLocales,
                },
                registeredApp,
                slug,
                store,
                versionId,
              },
            };
          }
        }
    
        // All supported locales are provided, use directly
        finalWhatsNew = whatsNew;
      } else if (text) {
        // Step 1: Get supported locales for the app (from registered app or fetch from API)
        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,
          };
        }
        let appStoreLocales: string[] = [];
        let googlePlayLocales: string[] = [];
    
        // Check if locales are already in registered app
        const {
          appStore: existingAppStoreLocales,
          googlePlay: existingGooglePlayLocales,
        } = collectSupportedLocales({ app: registeredApp, store });
    
        // If locales are missing, fetch from API
        if (includeAppStore && hasAppStore && bundleId) {
          if (existingAppStoreLocales.length > 0) {
            appStoreLocales = existingAppStoreLocales;
          } else if (config.appStore) {
            // Fetch from App Store API
            const appInfo = await appStoreService.fetchAppInfo(bundleId);
    
            if (appInfo.found) {
              if (appInfo.supportedLocales) {
                appStoreLocales = appInfo.supportedLocales;
                // Update registered app with fetched locales
                if (registeredApp.appStore) {
                  registeredApp.appStore.supportedLocales =
                    appInfo.supportedLocales;
                }
              }
            } else if (appInfo.error) {
              console.error(
                `[MCP]   ⚠️ Failed to fetch App Store locales: ${appInfo.error.message}`
              );
            }
          }
        }
    
        if (includeGooglePlay && hasGooglePlay && packageName) {
          if (existingGooglePlayLocales.length > 0) {
            googlePlayLocales = existingGooglePlayLocales;
          } else if (config.playStore?.serviceAccountJson) {
            // Fetch from Google Play API
            const appInfo = await googlePlayService.fetchAppInfo(packageName);
    
            if (appInfo.found) {
              if (appInfo.supportedLocales) {
                googlePlayLocales = appInfo.supportedLocales;
                // Update registered app with fetched locales
                if (registeredApp.googlePlay) {
                  registeredApp.googlePlay.supportedLocales =
                    appInfo.supportedLocales;
                }
              }
            } else if (appInfo.error) {
              console.error(
                `[MCP]   ⚠️ Failed to fetch Google Play locales: ${appInfo.error.message}`
              );
            }
          }
        }
    
        // Collect all unique locales that need translation
        const allLocales = new Set<string>();
        if (appStoreLocales.length > 0) {
          appStoreLocales.forEach((locale) => allLocales.add(locale));
        }
        if (googlePlayLocales.length > 0) {
          googlePlayLocales.forEach((locale) => allLocales.add(locale));
        }
    
        // If no locales found, return error
        if (allLocales.size === 0) {
          return {
            content: [
              {
                type: "text" as const,
                text: `❌ No supported locales found for the app. Please ensure the app is registered and has supported locales configured, or check authentication settings.`,
              },
            ],
          };
        }
    
        // Add source locale if not already in the set
        allLocales.add(sourceLocale);
    
        const targetLocales = Array.from(allLocales).filter(
          (locale) => locale !== sourceLocale
        );
    
        // Step 2: Return translation request
        // The calling LLM should perform the translation and call this function again with whatsNew
        return {
          content: [
            {
              type: "text" as const,
              text: `🌐 Translation Required
    
    **Source Text** (${sourceLocale}):
    ${text}
    
    **App Store Supported Locales** (${appStoreLocales.length}):
    ${appStoreLocales.length > 0 ? appStoreLocales.join(", ") : "N/A"}
    
    **Google Play Supported Locales** (${googlePlayLocales.length}):
    ${googlePlayLocales.length > 0 ? googlePlayLocales.join(", ") : "N/A"}
    
    **All Target Locales to Translate** (${targetLocales.length}):
    ${targetLocales.join(", ")}
    
    **Instructions**:
    Please translate the text to all target locales and call this function again with the \`whatsNew\` parameter containing all translations.
    
    Example:
    \`\`\`json
    {
      "app": "${slug}",
      "store": "${store}",
      "whatsNew": {
        "${sourceLocale}": "${text}",
        "ko": "번역된 텍스트",
        "ko-KR": "번역된 텍스트",
        "en-US": "Translated text",
        ...
      }
    }
    \`\`\`
    
    Note: App Store and Google Play may use different locale formats (e.g., "ko" vs "ko-KR"). Please provide translations for all supported locales.`,
            },
          ],
          _meta: {
            translationRequests: {
              appStore:
                appStoreLocales.length > 0
                  ? {
                      sourceText: text,
                      sourceLocale,
                      targetLocales: appStoreLocales.filter(
                        (l) => l !== sourceLocale
                      ),
                      store: "appStore" as const,
                    }
                  : undefined,
              googlePlay:
                googlePlayLocales.length > 0
                  ? {
                      sourceText: text,
                      sourceLocale,
                      targetLocales: googlePlayLocales.filter(
                        (l) => l !== sourceLocale
                      ),
                      store: "googlePlay" as const,
                    }
                  : undefined,
            },
            registeredApp,
            slug,
            store,
            versionId,
          },
        };
      } else {
        return {
          content: [
            {
              type: "text" as const,
              text: "❌ Either whatsNew or text is required. Provide whatsNew directly or text to translate to all supported languages.",
            },
          ],
        };
      }
    
      // Continue with update logic only if finalWhatsNew is set
      if (Object.keys(finalWhatsNew).length === 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: "❌ No release notes to update.",
            },
          ],
        };
      }
    
      const config = loadConfig();
    
      console.error(`[MCP] 📝 Updating release notes`);
      console.error(`[MCP]   Store: ${store}`);
      console.error(`[MCP]   App: ${slug}`);
      if (packageName) console.error(`[MCP]   Package Name: ${packageName}`);
      if (bundleId) console.error(`[MCP]   Bundle ID: ${bundleId}`);
      if (versionId) console.error(`[MCP]   Version ID: ${versionId}`);
    
      const results: string[] = [];
      const appStoreResults: string[] = [];
      const googlePlayResults: string[] = [];
    
      // Separate translations by store
      const { appStore: appStoreTranslations, googlePlay: googlePlayTranslations } =
        separateTranslationsByStore({
          translations: finalWhatsNew,
          app: registeredApp,
          sourceLocale,
          store,
        });
    
      console.error(
        `[MCP]   📝 Locales to update: ${Object.keys(finalWhatsNew).length}`
      );
      if (Object.keys(appStoreTranslations).length > 0) {
        console.error(
          `[MCP]   🍎 App Store locales: ${Object.keys(appStoreTranslations).join(
            ", "
          )}`
        );
      }
      if (Object.keys(googlePlayTranslations).length > 0) {
        console.error(
          `[MCP]   🤖 Google Play locales: ${Object.keys(
            googlePlayTranslations
          ).join(", ")}`
        );
      }
    
      // App Store update
      if ((store === "both" || store === "appStore") && bundleId) {
        console.error(`[MCP]   📤 Updating App Store release notes...`);
        if (!config.appStore) {
          appStoreResults.push("❌ App Store authentication not configured.");
        } else if (Object.keys(appStoreTranslations).length === 0) {
          appStoreResults.push(
            "⚠️ No translations available for App Store locales."
          );
        } else {
          const updateResult = await appStoreService.updateReleaseNotes(
            bundleId,
            appStoreTranslations,
            versionId,
            registeredApp.appStore?.supportedLocales
          );
    
          if (!updateResult.success) {
            appStoreResults.push(
              `❌ App Store release notes update failed: ${updateResult.error.message}`
            );
          } else {
            console.error(
              `[MCP]     ✅ Updated ${updateResult.data.updated.length} locales`
            );
            appStoreResults.push(
              ...formatReleaseNotesUpdate("App Store", updateResult.data)
            );
          }
        }
      }
    
      // Google Play update
      if ((store === "both" || store === "googlePlay") && packageName) {
        if (!config.playStore?.serviceAccountJson) {
          googlePlayResults.push("❌ Google Play authentication not configured.");
        } else if (Object.keys(googlePlayTranslations).length === 0) {
          googlePlayResults.push(
            "⚠️ No translations available for Google Play locales."
          );
        } else {
          console.error(`[MCP]   📤 Updating Google Play release notes...`);
          const updateResult = await googlePlayService.updateReleaseNotes(
            packageName,
            googlePlayTranslations,
            "production",
            registeredApp.googlePlay?.supportedLocales
          );
    
          if (!updateResult.success) {
            googlePlayResults.push(
              `❌ Google Play release notes update failed: ${updateResult.error.message}`
            );
          } else {
            console.error(
              `[MCP]     ✅ Updated ${updateResult.data.updated.length} locales`
            );
            googlePlayResults.push(
              ...formatReleaseNotesUpdate("Google Play", updateResult.data)
            );
          }
        }
      }
    
      // Combine results
      if (appStoreResults.length > 0) {
        results.push(`**🍎 App Store:**`);
        results.push(...appStoreResults.map((r) => `  ${r}`));
      }
      if (googlePlayResults.length > 0) {
        results.push(`**🤖 Google Play:**`);
        results.push(...googlePlayResults.map((r) => `  ${r}`));
      }
    
      if (results.length === 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: "⚠️ No store to update. Please check bundleId or packageName.",
            },
          ],
        };
      }
    
      return {
        content: [
          {
            type: "text" as const,
            text: `📝 Release Notes Update Results:\n\n${results.join("\n")}`,
          },
        ],
      };
    }
  • src/index.ts:346-380 (registration)
    Tool registration in the main MCP server file, linking the name 'release-update-notes' to the handler and defining the input schema.
    registerToolWithInfo(
      "release-update-notes",
      {
        description:
          "Update release notes (What's New) for App Store/Google Play version.",
        inputSchema: z.object({
          app: z.string().optional().describe("Registered app slug"),
          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)"),
          versionId: z
            .string()
            .optional()
            .describe(
              "App Store version ID (auto-detects editable version if not specified)"
            ),
          whatsNew: z
            .record(z.string(), z.string())
            .optional()
            .describe(
              'Release notes by locale (e.g., { "en-US": "Bug fixes", "ko": "Bug fixes" })'
            ),
          text: z
            .string()
            .optional()
            .describe("Source text to translate to all supported languages"),
          sourceLocale: z
            .string()
            .optional()
            .describe("Source locale (default: en-US)"),
        }),
      },
      handleUpdateNotes,
      "Release Management"
    );
  • Zod input schema defining parameters for the tool, including app identifiers, store, version, whatsNew translations, or text for auto-translation.
    inputSchema: z.object({
      app: z.string().optional().describe("Registered app slug"),
      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)"),
      versionId: z
        .string()
        .optional()
        .describe(
          "App Store version ID (auto-detects editable version if not specified)"
        ),
      whatsNew: z
        .record(z.string(), z.string())
        .optional()
        .describe(
          'Release notes by locale (e.g., { "en-US": "Bug fixes", "ko": "Bug fixes" })'
        ),
      text: z
        .string()
        .optional()
        .describe("Source text to translate to all supported languages"),
      sourceLocale: z
        .string()
        .optional()
        .describe("Source locale (default: en-US)"),
    }),
Behavior2/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. 'Update' implies a mutation operation, but the description doesn't specify required permissions, whether changes are reversible, rate limits, or what happens if parameters conflict. This leaves significant gaps for a tool with 8 parameters and no output schema.

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 directly states the tool's purpose without unnecessary words. It's appropriately sized and front-loaded, with every word earning its place.

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 (8 parameters, no annotations, no output schema), the description is insufficient. It doesn't address behavioral aspects like mutation effects, error handling, or return values, leaving the agent with incomplete context for proper tool invocation.

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 already documents all 8 parameters thoroughly. The description adds no additional parameter semantics beyond what's in the schema, such as explaining relationships between parameters like 'app', 'packageName', and 'bundleId'. Baseline 3 is appropriate when 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 ('Update release notes') and the resource ('for App Store/Google Play version'), providing specific verb+resource information. However, it doesn't distinguish this tool from sibling tools like 'release-pull-notes' or 'release-create', which would require explicit differentiation to earn a 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 like 'release-pull-notes' or 'release-create'. There's no mention of prerequisites, conditions for use, or when not to use it. The context is implied but not explicit.

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