analyze_top_keywords
Analyze top-ranking apps for a keyword to understand competitive positioning and market trends on iOS or Android app stores.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | The keyword or search term to analyze (e.g., 'meditation app', 'puzzle games'). | |
| platform | Yes | The platform (app store) to analyze ('ios' for Apple App Store, 'android' for Google Play Store). | |
| num | No | Number of top apps ranking for the keyword to analyze (1-50, default 10). These apps will be fetched with full details to provide comprehensive analysis. | |
| country | No | Two-letter country code for store localization. Default 'us'. | us |
| lang | No | Language code for results. Default 'en'. | en |
Implementation Reference
- server.js:272-432 (registration)Registration of the 'analyze_top_keywords' tool using McpServer.tool(), including schema and handler callback.server.tool( "analyze_top_keywords", { keyword: z.string().describe("The keyword or search term to analyze (e.g., 'meditation app', 'puzzle games')."), platform: z.enum(["ios", "android"]).describe("The platform (app store) to analyze ('ios' for Apple App Store, 'android' for Google Play Store)."), num: z.number().optional().default(10).describe("Number of top apps ranking for the keyword to analyze (1-50, default 10). These apps will be fetched with full details to provide comprehensive analysis."), country: z.string().length(2).optional().default("us").describe("Two-letter country code for store localization. Default 'us'."), lang: z.string().optional().default("en").describe("Language code for results. Default 'en'.") }, async ({ keyword, platform, num, country, lang }) => { try { let results = []; if (platform === "android") { // Get search results from Google Play Store results = await memoizedGplay.search({ term: keyword, num, country, lang, fullDetail: true }); } else { // Get search results from Apple App Store results = await memoizedAppStore.search({ term: keyword, num, country, lang }); // For Apple, we need to fetch full details for each app const fullDetailsPromises = results.map(app => { try { return memoizedAppStore.app({ id: app.id, country, lang, ratings: true }); } catch (err) { console.error(`Error fetching details for app ${app.id}:`, err); return app; // Return original data if full details fetch fails } }); // Wait for all detail requests to complete results = await Promise.all(fullDetailsPromises); } // Normalize and extract key metrics const normalizedApps = results.map(app => { if (platform === "android") { return { appId: app.appId, title: app.title, developer: app.developer, developerId: app.developerId, installs: app.installs, minInstalls: app.minInstalls, score: app.score, ratings: app.ratings, free: app.free, price: app.price, currency: app.currency, category: app.genre, url: app.url, icon: app.icon }; } else { return { appId: app.appId, title: app.title, developer: app.developer, developerId: app.developerId, score: app.score, ratings: app.ratings || 0, free: app.free, price: app.price, currency: app.currency, category: app.primaryGenre, url: app.url, icon: app.icon }; } }); // Calculate brand presence metrics const developerCounts = {}; normalizedApps.forEach(app => { developerCounts[app.developer] = (developerCounts[app.developer] || 0) + 1; }); // Sort developers by number of apps in results const sortedDevelopers = Object.entries(developerCounts) .sort((a, b) => b[1] - a[1]) .map(entry => entry[0]); // Calculate average ratings and other metrics const totalApps = normalizedApps.length; const avgRating = normalizedApps.reduce((sum, app) => sum + (app.score || 0), 0) / totalApps; const paidApps = normalizedApps.filter(app => !app.free); const paidPercentage = (paidApps.length / totalApps) * 100; // Check for big brand presence (simplified algorithm) // Here we're assuming the top 2 developers with most apps are "big brands" const topBrands = sortedDevelopers.slice(0, 2); const topBrandAppsCount = topBrands.reduce((count, brand) => count + developerCounts[brand], 0); const brandDominance = topBrandAppsCount / totalApps; // Determine competition level let competitionLevel; if (brandDominance > 0.7) { competitionLevel = "Low - dominated by major brands"; } else if (brandDominance > 0.4) { competitionLevel = "Medium - mix of major brands and independents"; } else { competitionLevel = "High - diverse set of developers"; } // Create category distribution const categoryDistribution = {}; normalizedApps.forEach(app => { const category = app.category; if (category) { categoryDistribution[category] = (categoryDistribution[category] || 0) + 1; } }); return { content: [{ type: "text", text: JSON.stringify({ keyword, platform, topApps: normalizedApps, brandPresence: { topBrands, brandDominance: parseFloat(brandDominance.toFixed(2)), competitionLevel }, metrics: { totalApps, averageRating: parseFloat(avgRating.toFixed(2)), paidAppsPercentage: parseFloat(paidPercentage.toFixed(2)), categoryDistribution } }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error.message, keyword, platform }, null, 2) }], isError: true }; } } );
- server.js:274-280 (schema)Zod input schema defining parameters: keyword (string), platform (ios/android), num (number, optional), country (string, optional), lang (string, optional). Used for validation in MCP tool calls.{ keyword: z.string().describe("The keyword or search term to analyze (e.g., 'meditation app', 'puzzle games')."), platform: z.enum(["ios", "android"]).describe("The platform (app store) to analyze ('ios' for Apple App Store, 'android' for Google Play Store)."), num: z.number().optional().default(10).describe("Number of top apps ranking for the keyword to analyze (1-50, default 10). These apps will be fetched with full details to provide comprehensive analysis."), country: z.string().length(2).optional().default("us").describe("Two-letter country code for store localization. Default 'us'."), lang: z.string().optional().default("en").describe("Language code for results. Default 'en'.") },
- server.js:281-431 (handler)Handler function performs keyword search on specified platform using memoized scrapers (gplay/appStore), fetches top apps, normalizes data, computes brand presence/dominance, competition level, average ratings, paid app percentage, category distribution, and returns structured JSON analysis.async ({ keyword, platform, num, country, lang }) => { try { let results = []; if (platform === "android") { // Get search results from Google Play Store results = await memoizedGplay.search({ term: keyword, num, country, lang, fullDetail: true }); } else { // Get search results from Apple App Store results = await memoizedAppStore.search({ term: keyword, num, country, lang }); // For Apple, we need to fetch full details for each app const fullDetailsPromises = results.map(app => { try { return memoizedAppStore.app({ id: app.id, country, lang, ratings: true }); } catch (err) { console.error(`Error fetching details for app ${app.id}:`, err); return app; // Return original data if full details fetch fails } }); // Wait for all detail requests to complete results = await Promise.all(fullDetailsPromises); } // Normalize and extract key metrics const normalizedApps = results.map(app => { if (platform === "android") { return { appId: app.appId, title: app.title, developer: app.developer, developerId: app.developerId, installs: app.installs, minInstalls: app.minInstalls, score: app.score, ratings: app.ratings, free: app.free, price: app.price, currency: app.currency, category: app.genre, url: app.url, icon: app.icon }; } else { return { appId: app.appId, title: app.title, developer: app.developer, developerId: app.developerId, score: app.score, ratings: app.ratings || 0, free: app.free, price: app.price, currency: app.currency, category: app.primaryGenre, url: app.url, icon: app.icon }; } }); // Calculate brand presence metrics const developerCounts = {}; normalizedApps.forEach(app => { developerCounts[app.developer] = (developerCounts[app.developer] || 0) + 1; }); // Sort developers by number of apps in results const sortedDevelopers = Object.entries(developerCounts) .sort((a, b) => b[1] - a[1]) .map(entry => entry[0]); // Calculate average ratings and other metrics const totalApps = normalizedApps.length; const avgRating = normalizedApps.reduce((sum, app) => sum + (app.score || 0), 0) / totalApps; const paidApps = normalizedApps.filter(app => !app.free); const paidPercentage = (paidApps.length / totalApps) * 100; // Check for big brand presence (simplified algorithm) // Here we're assuming the top 2 developers with most apps are "big brands" const topBrands = sortedDevelopers.slice(0, 2); const topBrandAppsCount = topBrands.reduce((count, brand) => count + developerCounts[brand], 0); const brandDominance = topBrandAppsCount / totalApps; // Determine competition level let competitionLevel; if (brandDominance > 0.7) { competitionLevel = "Low - dominated by major brands"; } else if (brandDominance > 0.4) { competitionLevel = "Medium - mix of major brands and independents"; } else { competitionLevel = "High - diverse set of developers"; } // Create category distribution const categoryDistribution = {}; normalizedApps.forEach(app => { const category = app.category; if (category) { categoryDistribution[category] = (categoryDistribution[category] || 0) + 1; } }); return { content: [{ type: "text", text: JSON.stringify({ keyword, platform, topApps: normalizedApps, brandPresence: { topBrands, brandDominance: parseFloat(brandDominance.toFixed(2)), competitionLevel }, metrics: { totalApps, averageRating: parseFloat(avgRating.toFixed(2)), paidAppsPercentage: parseFloat(paidPercentage.toFixed(2)), categoryDistribution } }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error.message, keyword, platform }, null, 2) }], isError: true }; } }