fetch_reviews
Retrieve app reviews in bulk from Google Play Store and Apple App Store by specifying app ID, platform, and filters like region, language, and sorting criteria. Simplifies review analysis and feedback collection for app developers.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| appId | Yes | The unique identifier for the app (Android package name, iOS numeric ID or bundle ID). | |
| country | No | Two-letter country code for the App Store/Play Store region. Default 'us'. | us |
| lang | No | Language code for reviews. Default 'en'. | en |
| num | No | Number of reviews to fetch (1-1000, default 100). Note: Actual number may be less due to API limitations. For iOS, limited to 10 pages max. | |
| platform | Yes | The platform of the app ('ios' or 'android'). | |
| sort | No | Sorting order for reviews: 'newest', 'rating', 'helpfulness'. Default 'newest'. | newest |
Implementation Reference
- server.js:807-975 (registration)Registration of the 'fetch_reviews' MCP tool, including inline Zod schema and async handler function.server.tool( "fetch_reviews", { appId: z.string().describe("The unique identifier for the app (Android package name, iOS numeric ID or bundle ID)."), platform: z.enum(["ios", "android"]).describe("The platform of the app ('ios' or 'android')."), num: z.number().optional().default(100).describe("Number of reviews to fetch (1-1000, default 100). Note: Actual number may be less due to API limitations. For iOS, limited to 10 pages max."), country: z.string().length(2).optional().default("us").describe("Two-letter country code for the App Store/Play Store region. Default 'us'."), lang: z.string().optional().default("en").describe("Language code for reviews. Default 'en'."), sort: z.enum(["newest", "rating", "helpfulness"]).optional().default("newest").describe("Sorting order for reviews: 'newest', 'rating', 'helpfulness'. Default 'newest'.") }, async ({ appId, platform, num, country, lang, sort }) => { try { let reviews = []; // Fetch reviews from the appropriate platform if (platform === "android") { let sortType; switch (sort) { case "newest": sortType = gplay.sort.NEWEST; break; case "rating": sortType = gplay.sort.RATING; break; case "helpfulness": sortType = gplay.sort.HELPFULNESS; break; default: sortType = gplay.sort.NEWEST; } const result = await memoizedGplay.reviews({ appId, num: Math.min(num, 1000), // Limit to 1000 reviews max sort: sortType, country, lang }); reviews = result.data || []; // Android reviews have developer replies included if available return { content: [{ type: "text", text: JSON.stringify({ appId, platform, count: reviews.length, reviews: reviews.map(review => ({ id: review.id, userName: review.userName, userImage: review.userImage, score: review.score, scoreText: review.scoreText, title: review.title || "", text: review.text, date: review.date, url: review.url, version: review.version, thumbsUp: review.thumbsUp, replyDate: review.replyDate, replyText: review.replyText, hasDeveloperResponse: !!review.replyText })) }, null, 2) }] }; } else { let page = 1; let allReviews = []; let sortType; switch (sort) { case "newest": sortType = appStore.sort.RECENT; break; case "helpfulness": sortType = appStore.sort.HELPFUL; break; default: sortType = appStore.sort.RECENT; } // For iOS, we might need to fetch multiple pages while (allReviews.length < num && page <= 10) { // App Store only allows 10 pages try { // For iOS apps, we need to use id instead of appId let iosParams = {}; // Check if the appId is already a numeric ID if (/^\d+$/.test(appId)) { iosParams = { id: appId, page, sort: sortType, country }; } else { // First we need to fetch the app to get its numeric ID try { const appDetails = await memoizedAppStore.app({ appId, country }); iosParams = { id: appDetails.id.toString(), page, sort: sortType, country }; } catch (appError) { console.error(`Could not fetch app details for ${appId}:`, appError.message); break; } } const pageReviews = await memoizedAppStore.reviews(iosParams); if (!pageReviews || pageReviews.length === 0) { break; // No more reviews } allReviews = [...allReviews, ...pageReviews]; page++; } catch (err) { console.error(`Error fetching reviews page ${page}:`, err); break; } } reviews = allReviews.slice(0, num); // iOS reviews don't include developer responses in the API return { content: [{ type: "text", text: JSON.stringify({ appId, platform, count: reviews.length, reviews: reviews.map(review => ({ id: review.id, userName: review.userName, userUrl: review.userUrl, score: review.score, title: review.title, text: review.text, date: review.updated, version: review.version, url: review.url, hasDeveloperResponse: false // App Store API doesn't provide this info })) }, null, 2) }] }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error.message, appId, platform }, null, 2) }], isError: true }; } } );
- server.js:809-815 (schema)Input schema using Zod for validating parameters: appId (string), platform (ios/android), num (number, default 100), country (2-letter code, default 'us'), lang (default 'en'), sort (newest/rating/helpfulness, default 'newest').{ appId: z.string().describe("The unique identifier for the app (Android package name, iOS numeric ID or bundle ID)."), platform: z.enum(["ios", "android"]).describe("The platform of the app ('ios' or 'android')."), num: z.number().optional().default(100).describe("Number of reviews to fetch (1-1000, default 100). Note: Actual number may be less due to API limitations. For iOS, limited to 10 pages max."), country: z.string().length(2).optional().default("us").describe("Two-letter country code for the App Store/Play Store region. Default 'us'."), lang: z.string().optional().default("en").describe("Language code for reviews. Default 'en'."), sort: z.enum(["newest", "rating", "helpfulness"]).optional().default("newest").describe("Sorting order for reviews: 'newest', 'rating', 'helpfulness'. Default 'newest'.")
- server.js:817-974 (handler)Core handler logic: Fetches raw reviews via platform scrapers (google-play-scraper for Android with sort options, app-store-scraper for iOS with multi-page fetching and ID resolution). Standardizes review objects (id, userName, score, text, date, version, replyText/hasDeveloperResponse). Returns structured JSON or error.async ({ appId, platform, num, country, lang, sort }) => { try { let reviews = []; // Fetch reviews from the appropriate platform if (platform === "android") { let sortType; switch (sort) { case "newest": sortType = gplay.sort.NEWEST; break; case "rating": sortType = gplay.sort.RATING; break; case "helpfulness": sortType = gplay.sort.HELPFULNESS; break; default: sortType = gplay.sort.NEWEST; } const result = await memoizedGplay.reviews({ appId, num: Math.min(num, 1000), // Limit to 1000 reviews max sort: sortType, country, lang }); reviews = result.data || []; // Android reviews have developer replies included if available return { content: [{ type: "text", text: JSON.stringify({ appId, platform, count: reviews.length, reviews: reviews.map(review => ({ id: review.id, userName: review.userName, userImage: review.userImage, score: review.score, scoreText: review.scoreText, title: review.title || "", text: review.text, date: review.date, url: review.url, version: review.version, thumbsUp: review.thumbsUp, replyDate: review.replyDate, replyText: review.replyText, hasDeveloperResponse: !!review.replyText })) }, null, 2) }] }; } else { let page = 1; let allReviews = []; let sortType; switch (sort) { case "newest": sortType = appStore.sort.RECENT; break; case "helpfulness": sortType = appStore.sort.HELPFUL; break; default: sortType = appStore.sort.RECENT; } // For iOS, we might need to fetch multiple pages while (allReviews.length < num && page <= 10) { // App Store only allows 10 pages try { // For iOS apps, we need to use id instead of appId let iosParams = {}; // Check if the appId is already a numeric ID if (/^\d+$/.test(appId)) { iosParams = { id: appId, page, sort: sortType, country }; } else { // First we need to fetch the app to get its numeric ID try { const appDetails = await memoizedAppStore.app({ appId, country }); iosParams = { id: appDetails.id.toString(), page, sort: sortType, country }; } catch (appError) { console.error(`Could not fetch app details for ${appId}:`, appError.message); break; } } const pageReviews = await memoizedAppStore.reviews(iosParams); if (!pageReviews || pageReviews.length === 0) { break; // No more reviews } allReviews = [...allReviews, ...pageReviews]; page++; } catch (err) { console.error(`Error fetching reviews page ${page}:`, err); break; } } reviews = allReviews.slice(0, num); // iOS reviews don't include developer responses in the API return { content: [{ type: "text", text: JSON.stringify({ appId, platform, count: reviews.length, reviews: reviews.map(review => ({ id: review.id, userName: review.userName, userUrl: review.userUrl, score: review.score, title: review.title, text: review.text, date: review.updated, version: review.version, url: review.url, hasDeveloperResponse: false // App Store API doesn't provide this info })) }, null, 2) }] }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error.message, appId, platform }, null, 2) }], isError: true }; } }