Skip to main content
Glama
appreply-co

mcp-appstore

by appreply-co

analyze_reviews

Analyzes app store reviews to extract insights on user feedback, sentiment, and common issues for iOS and Android applications.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
appIdYesThe unique identifier for the app (Android package name, iOS numeric ID or bundle ID).
platformYesThe platform of the app ('ios' or 'android').
numNoTarget number of reviews to analyze (1-1000, default 100). Note: Actual number may be less due to API limitations or available reviews. For iOS, fetching requires multiple requests and is capped at page 10.
countryNoTwo-letter country code for reviews localization. Default 'us'.us
langNoLanguage code for filtering reviews (results may be less accurate if language doesn't match review content). Default 'en'.en
sortNoSorting order for reviews: 'newest', 'rating' (highest/lowest first, platform dependent), 'helpfulness'. Default 'newest'.newest

Implementation Reference

  • Core handler function for 'analyze_reviews' tool. Fetches recent reviews for the specified app on Android or iOS platforms using memoized scrapers. Performs keyword-based sentiment analysis, extracts frequent keywords, computes sentiment and rating breakdowns, identifies common user themes (bugs, pricing, UX), and highlights recent negative issues. Returns comprehensive JSON analysis.
    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 || [];
        } 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);
        }
        
        // Very basic sentiment analysis functions
        function analyzeSentiment(text) {
          if (!text) return 'neutral';
          
          // Define simple positive and negative word lists
          const positiveWords = [
            'good', 'great', 'excellent', 'awesome', 'amazing', 'love', 'best',
            'perfect', 'fantastic', 'wonderful', 'happy', 'easy', 'helpful',
            'recommend', 'recommended', 'nice', 'beautiful', 'fun', 'enjoy',
            'worth', 'favorite', 'improvement', 'improved', 'better', 'useful'
          ];
          
          const negativeWords = [
            'bad', 'terrible', 'awful', 'horrible', 'poor', 'worst', 'waste',
            'useless', 'difficult', 'hate', 'crash', 'bug', 'problem', 'issue',
            'disappointing', 'disappointed', 'fix', 'error', 'fail', 'fails',
            'wrong', 'frustrating', 'slow', 'expensive', 'annoying', 'boring'
          ];
          
          // Convert text to lowercase and split into words
          const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
          
          // Count positive and negative words
          let positiveCount = 0;
          let negativeCount = 0;
          
          words.forEach(word => {
            if (positiveWords.includes(word)) positiveCount++;
            if (negativeWords.includes(word)) negativeCount++;
          });
          
          // Determine sentiment based on counts
          if (positiveCount > negativeCount * 2) return 'positive';
          if (negativeCount > positiveCount * 2) return 'negative';
          if (positiveCount > negativeCount) return 'somewhat positive';
          if (negativeCount > positiveCount) return 'somewhat negative';
          return 'neutral';
        }
        
        // Extract keywords from text
        function extractKeywords(text) {
          if (!text) return [];
          
          // Common words to exclude
          const stopWords = [
            'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
            'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him',
            'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its',
            'itself', 'they', 'them', 'their', 'theirs', 'themselves',
            'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those',
            'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have',
            'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an',
            'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while',
            'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between',
            'into', 'through', 'during', 'before', 'after', 'above', 'below',
            'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over',
            'under', 'again', 'further', 'then', 'once', 'here', 'there',
            'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each',
            'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor',
            'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
            's', 't', 'can', 'will', 'just', 'don', 'should', 'now', 'app'
          ];
          
          // Extract words, remove stop words, and filter out short words
          const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
          return words.filter(word => 
            !stopWords.includes(word) && word.length > 3
          );
        }
        
        // Process all reviews
        const processedReviews = reviews.map(review => {
          const reviewText = platform === 'android' ? review.text : review.text;
          const reviewScore = platform === 'android' ? review.score : review.score;
          
          const sentiment = analyzeSentiment(reviewText);
          const keywords = extractKeywords(reviewText);
          
          return {
            id: review.id,
            text: reviewText,
            score: reviewScore,
            sentiment,
            keywords,
            date: platform === 'android' ? review.date : review.updated
          };
        });
        
        // Calculate sentiment distribution
        const sentimentCounts = {
          positive: 0,
          "somewhat positive": 0,
          neutral: 0,
          "somewhat negative": 0,
          negative: 0
        };
        
        processedReviews.forEach(review => {
          sentimentCounts[review.sentiment] = (sentimentCounts[review.sentiment] || 0) + 1;
        });
        
        const totalReviews = processedReviews.length;
        const sentimentBreakdown = {};
        
        Object.keys(sentimentCounts).forEach(sentiment => {
          const percentage = totalReviews ? (sentimentCounts[sentiment] / totalReviews) * 100 : 0;
          sentimentBreakdown[sentiment] = parseFloat(percentage.toFixed(2));
        });
        
        // Calculate keyword frequency
        const allKeywords = processedReviews.flatMap(review => review.keywords);
        const keywordFrequency = {};
        
        allKeywords.forEach(keyword => {
          keywordFrequency[keyword] = (keywordFrequency[keyword] || 0) + 1;
        });
        
        // Sort keywords by frequency and take top 20
        const topKeywords = Object.entries(keywordFrequency)
          .sort((a, b) => b[1] - a[1])
          .slice(0, 20)
          .reduce((obj, [key, value]) => {
            obj[key] = value;
            return obj;
          }, {});
        
        // Identify common themes
        const commonThemes = [];
        
        // Look for bug/crash mentions
        const bugKeywords = ['bug', 'crash', 'freezes', 'frozen', 'stuck', 'error'];
        const hasBugTheme = bugKeywords.some(word => 
          Object.keys(keywordFrequency).some(kw => kw.includes(word))
        );
        
        if (hasBugTheme) {
          commonThemes.push({
            theme: "Stability Issues",
            description: "Users are reporting crashes, bugs, or freezes"
          });
        }
        
        // Look for pricing/cost mentions
        const pricingKeywords = ['price', 'cost', 'expensive', 'cheap', 'free', 'subscription', 'payment'];
        const hasPricingTheme = pricingKeywords.some(word => 
          Object.keys(keywordFrequency).some(kw => kw.includes(word))
        );
        
        if (hasPricingTheme) {
          commonThemes.push({
            theme: "Pricing Concerns",
            description: "Users are discussing price or subscription costs"
          });
        }
        
        // Look for UX/UI feedback
        const uxKeywords = ['interface', 'design', 'layout', 'ugly', 'beautiful', 'easy', 'difficult', 'confusing'];
        const hasUxTheme = uxKeywords.some(word => 
          Object.keys(keywordFrequency).some(kw => kw.includes(word))
        );
        
        if (hasUxTheme) {
          commonThemes.push({
            theme: "User Experience",
            description: "Users are commenting on the app's design or usability"
          });
        }
        
        // Identify recent issues (from negative reviews in the last 7 days)
        const oneWeekAgo = new Date();
        oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
        
        const recentNegativeReviews = processedReviews.filter(review => {
          const reviewDate = new Date(review.date);
          return (
            reviewDate >= oneWeekAgo &&
            (review.sentiment === 'negative' || review.sentiment === 'somewhat negative')
          );
        });
        
        const recentIssuesKeywords = recentNegativeReviews.flatMap(review => review.keywords);
        const recentIssuesFrequency = {};
        
        recentIssuesKeywords.forEach(keyword => {
          recentIssuesFrequency[keyword] = (recentIssuesFrequency[keyword] || 0) + 1;
        });
        
        // Sort recent issues by frequency and take top 10
        const topRecentIssues = Object.entries(recentIssuesFrequency)
          .sort((a, b) => b[1] - a[1])
          .slice(0, 10)
          .reduce((obj, [key, value]) => {
            obj[key] = value;
            return obj;
          }, {});
        
        // Calculate rating distribution
        const ratingDistribution = {
          "1": 0,
          "2": 0,
          "3": 0,
          "4": 0,
          "5": 0
        };
        
        processedReviews.forEach(review => {
          const score = Math.floor(review.score);
          if (score >= 1 && score <= 5) {
            ratingDistribution[score] = (ratingDistribution[score] || 0) + 1;
          }
        });
        
        return {
          content: [{ 
            type: "text", 
            text: JSON.stringify({
              appId,
              platform,
              totalReviewsAnalyzed: processedReviews.length,
              analysis: {
                sentimentBreakdown,
                keywordFrequency: topKeywords,
                ratingDistribution,
                commonThemes,
                recentIssues: topRecentIssues,
                topPositiveKeywords: Object.entries(keywordFrequency)
                  .filter(([key, value]) => 
                    processedReviews.some(r => 
                      r.sentiment === 'positive' && r.keywords.includes(key)
                    )
                  )
                  .sort((a, b) => b[1] - a[1])
                  .slice(0, 10)
                  .reduce((obj, [key, value]) => {
                    obj[key] = value;
                    return obj;
                  }, {}),
                topNegativeKeywords: Object.entries(keywordFrequency)
                  .filter(([key, value]) => 
                    processedReviews.some(r => 
                      r.sentiment === 'negative' && r.keywords.includes(key)
                    )
                  )
                  .sort((a, b) => b[1] - a[1])
                  .slice(0, 10)
                  .reduce((obj, [key, value]) => {
                    obj[key] = value;
                    return obj;
                  }, {})
              }
            }, null, 2)
          }]
        };
      } catch (error) {
        return {
          content: [{ 
            type: "text", 
            text: JSON.stringify({
              error: error.message,
              appId,
              platform
            }, null, 2)
          }],
          isError: true
        };
      }
    }
  • Zod input schema defining parameters for the analyze_reviews tool: appId (required), platform (ios/android), num reviews (default 100), country (default us), lang (default en), sort order (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("Target number of reviews to analyze (1-1000, default 100). Note: Actual number may be less due to API limitations or available reviews. For iOS, fetching requires multiple requests and is capped at page 10."),
      country: z.string().length(2).optional().default("us").describe("Two-letter country code for reviews localization. Default 'us'."),
      lang: z.string().optional().default("en").describe("Language code for filtering reviews (results may be less accurate if language doesn't match review content). Default 'en'."),
      sort: z.enum(["newest", "rating", "helpfulness"]).optional().default("newest").describe("Sorting order for reviews: 'newest', 'rating' (highest/lowest first, platform dependent), 'helpfulness'. Default 'newest'.")
  • server.js:434-804 (registration)
    MCP server tool registration call that defines the 'analyze_reviews' tool with its name, input schema, and handler function.
    server.tool(
      "analyze_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("Target number of reviews to analyze (1-1000, default 100). Note: Actual number may be less due to API limitations or available reviews. For iOS, fetching requires multiple requests and is capped at page 10."),
        country: z.string().length(2).optional().default("us").describe("Two-letter country code for reviews localization. Default 'us'."),
        lang: z.string().optional().default("en").describe("Language code for filtering reviews (results may be less accurate if language doesn't match review content). Default 'en'."),
        sort: z.enum(["newest", "rating", "helpfulness"]).optional().default("newest").describe("Sorting order for reviews: 'newest', 'rating' (highest/lowest first, platform dependent), '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 || [];
          } 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);
          }
          
          // Very basic sentiment analysis functions
          function analyzeSentiment(text) {
            if (!text) return 'neutral';
            
            // Define simple positive and negative word lists
            const positiveWords = [
              'good', 'great', 'excellent', 'awesome', 'amazing', 'love', 'best',
              'perfect', 'fantastic', 'wonderful', 'happy', 'easy', 'helpful',
              'recommend', 'recommended', 'nice', 'beautiful', 'fun', 'enjoy',
              'worth', 'favorite', 'improvement', 'improved', 'better', 'useful'
            ];
            
            const negativeWords = [
              'bad', 'terrible', 'awful', 'horrible', 'poor', 'worst', 'waste',
              'useless', 'difficult', 'hate', 'crash', 'bug', 'problem', 'issue',
              'disappointing', 'disappointed', 'fix', 'error', 'fail', 'fails',
              'wrong', 'frustrating', 'slow', 'expensive', 'annoying', 'boring'
            ];
            
            // Convert text to lowercase and split into words
            const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
            
            // Count positive and negative words
            let positiveCount = 0;
            let negativeCount = 0;
            
            words.forEach(word => {
              if (positiveWords.includes(word)) positiveCount++;
              if (negativeWords.includes(word)) negativeCount++;
            });
            
            // Determine sentiment based on counts
            if (positiveCount > negativeCount * 2) return 'positive';
            if (negativeCount > positiveCount * 2) return 'negative';
            if (positiveCount > negativeCount) return 'somewhat positive';
            if (negativeCount > positiveCount) return 'somewhat negative';
            return 'neutral';
          }
          
          // Extract keywords from text
          function extractKeywords(text) {
            if (!text) return [];
            
            // Common words to exclude
            const stopWords = [
              'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
              'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him',
              'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its',
              'itself', 'they', 'them', 'their', 'theirs', 'themselves',
              'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those',
              'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have',
              'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an',
              'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while',
              'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between',
              'into', 'through', 'during', 'before', 'after', 'above', 'below',
              'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over',
              'under', 'again', 'further', 'then', 'once', 'here', 'there',
              'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each',
              'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor',
              'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
              's', 't', 'can', 'will', 'just', 'don', 'should', 'now', 'app'
            ];
            
            // Extract words, remove stop words, and filter out short words
            const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
            return words.filter(word => 
              !stopWords.includes(word) && word.length > 3
            );
          }
          
          // Process all reviews
          const processedReviews = reviews.map(review => {
            const reviewText = platform === 'android' ? review.text : review.text;
            const reviewScore = platform === 'android' ? review.score : review.score;
            
            const sentiment = analyzeSentiment(reviewText);
            const keywords = extractKeywords(reviewText);
            
            return {
              id: review.id,
              text: reviewText,
              score: reviewScore,
              sentiment,
              keywords,
              date: platform === 'android' ? review.date : review.updated
            };
          });
          
          // Calculate sentiment distribution
          const sentimentCounts = {
            positive: 0,
            "somewhat positive": 0,
            neutral: 0,
            "somewhat negative": 0,
            negative: 0
          };
          
          processedReviews.forEach(review => {
            sentimentCounts[review.sentiment] = (sentimentCounts[review.sentiment] || 0) + 1;
          });
          
          const totalReviews = processedReviews.length;
          const sentimentBreakdown = {};
          
          Object.keys(sentimentCounts).forEach(sentiment => {
            const percentage = totalReviews ? (sentimentCounts[sentiment] / totalReviews) * 100 : 0;
            sentimentBreakdown[sentiment] = parseFloat(percentage.toFixed(2));
          });
          
          // Calculate keyword frequency
          const allKeywords = processedReviews.flatMap(review => review.keywords);
          const keywordFrequency = {};
          
          allKeywords.forEach(keyword => {
            keywordFrequency[keyword] = (keywordFrequency[keyword] || 0) + 1;
          });
          
          // Sort keywords by frequency and take top 20
          const topKeywords = Object.entries(keywordFrequency)
            .sort((a, b) => b[1] - a[1])
            .slice(0, 20)
            .reduce((obj, [key, value]) => {
              obj[key] = value;
              return obj;
            }, {});
          
          // Identify common themes
          const commonThemes = [];
          
          // Look for bug/crash mentions
          const bugKeywords = ['bug', 'crash', 'freezes', 'frozen', 'stuck', 'error'];
          const hasBugTheme = bugKeywords.some(word => 
            Object.keys(keywordFrequency).some(kw => kw.includes(word))
          );
          
          if (hasBugTheme) {
            commonThemes.push({
              theme: "Stability Issues",
              description: "Users are reporting crashes, bugs, or freezes"
            });
          }
          
          // Look for pricing/cost mentions
          const pricingKeywords = ['price', 'cost', 'expensive', 'cheap', 'free', 'subscription', 'payment'];
          const hasPricingTheme = pricingKeywords.some(word => 
            Object.keys(keywordFrequency).some(kw => kw.includes(word))
          );
          
          if (hasPricingTheme) {
            commonThemes.push({
              theme: "Pricing Concerns",
              description: "Users are discussing price or subscription costs"
            });
          }
          
          // Look for UX/UI feedback
          const uxKeywords = ['interface', 'design', 'layout', 'ugly', 'beautiful', 'easy', 'difficult', 'confusing'];
          const hasUxTheme = uxKeywords.some(word => 
            Object.keys(keywordFrequency).some(kw => kw.includes(word))
          );
          
          if (hasUxTheme) {
            commonThemes.push({
              theme: "User Experience",
              description: "Users are commenting on the app's design or usability"
            });
          }
          
          // Identify recent issues (from negative reviews in the last 7 days)
          const oneWeekAgo = new Date();
          oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
          
          const recentNegativeReviews = processedReviews.filter(review => {
            const reviewDate = new Date(review.date);
            return (
              reviewDate >= oneWeekAgo &&
              (review.sentiment === 'negative' || review.sentiment === 'somewhat negative')
            );
          });
          
          const recentIssuesKeywords = recentNegativeReviews.flatMap(review => review.keywords);
          const recentIssuesFrequency = {};
          
          recentIssuesKeywords.forEach(keyword => {
            recentIssuesFrequency[keyword] = (recentIssuesFrequency[keyword] || 0) + 1;
          });
          
          // Sort recent issues by frequency and take top 10
          const topRecentIssues = Object.entries(recentIssuesFrequency)
            .sort((a, b) => b[1] - a[1])
            .slice(0, 10)
            .reduce((obj, [key, value]) => {
              obj[key] = value;
              return obj;
            }, {});
          
          // Calculate rating distribution
          const ratingDistribution = {
            "1": 0,
            "2": 0,
            "3": 0,
            "4": 0,
            "5": 0
          };
          
          processedReviews.forEach(review => {
            const score = Math.floor(review.score);
            if (score >= 1 && score <= 5) {
              ratingDistribution[score] = (ratingDistribution[score] || 0) + 1;
            }
          });
          
          return {
            content: [{ 
              type: "text", 
              text: JSON.stringify({
                appId,
                platform,
                totalReviewsAnalyzed: processedReviews.length,
                analysis: {
                  sentimentBreakdown,
                  keywordFrequency: topKeywords,
                  ratingDistribution,
                  commonThemes,
                  recentIssues: topRecentIssues,
                  topPositiveKeywords: Object.entries(keywordFrequency)
                    .filter(([key, value]) => 
                      processedReviews.some(r => 
                        r.sentiment === 'positive' && r.keywords.includes(key)
                      )
                    )
                    .sort((a, b) => b[1] - a[1])
                    .slice(0, 10)
                    .reduce((obj, [key, value]) => {
                      obj[key] = value;
                      return obj;
                    }, {}),
                  topNegativeKeywords: Object.entries(keywordFrequency)
                    .filter(([key, value]) => 
                      processedReviews.some(r => 
                        r.sentiment === 'negative' && r.keywords.includes(key)
                      )
                    )
                    .sort((a, b) => b[1] - a[1])
                    .slice(0, 10)
                    .reduce((obj, [key, value]) => {
                      obj[key] = value;
                      return obj;
                    }, {})
                }
              }, null, 2)
            }]
          };
        } catch (error) {
          return {
            content: [{ 
              type: "text", 
              text: JSON.stringify({
                error: error.message,
                appId,
                platform
              }, null, 2)
            }],
            isError: true
          };
        }
      }
    );
  • Inline helper function for basic keyword-based sentiment analysis used in review processing. Uses predefined positive/negative word lists to classify reviews.
    function analyzeSentiment(text) {
      if (!text) return 'neutral';
      
      // Define simple positive and negative word lists
      const positiveWords = [
        'good', 'great', 'excellent', 'awesome', 'amazing', 'love', 'best',
        'perfect', 'fantastic', 'wonderful', 'happy', 'easy', 'helpful',
        'recommend', 'recommended', 'nice', 'beautiful', 'fun', 'enjoy',
        'worth', 'favorite', 'improvement', 'improved', 'better', 'useful'
      ];
      
      const negativeWords = [
        'bad', 'terrible', 'awful', 'horrible', 'poor', 'worst', 'waste',
        'useless', 'difficult', 'hate', 'crash', 'bug', 'problem', 'issue',
        'disappointing', 'disappointed', 'fix', 'error', 'fail', 'fails',
        'wrong', 'frustrating', 'slow', 'expensive', 'annoying', 'boring'
      ];
      
      // Convert text to lowercase and split into words
      const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
      
      // Count positive and negative words
      let positiveCount = 0;
      let negativeCount = 0;
      
      words.forEach(word => {
        if (positiveWords.includes(word)) positiveCount++;
        if (negativeWords.includes(word)) negativeCount++;
      });
      
      // Determine sentiment based on counts
      if (positiveCount > negativeCount * 2) return 'positive';
      if (negativeCount > positiveCount * 2) return 'negative';
      if (positiveCount > negativeCount) return 'somewhat positive';
      if (negativeCount > positiveCount) return 'somewhat negative';
      return 'neutral';
    }
  • Inline helper for keyword extraction from review text. Filters out common English stop words and short terms (<4 chars).
    function extractKeywords(text) {
      if (!text) return [];
      
      // Common words to exclude
      const stopWords = [
        'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
        'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him',
        'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its',
        'itself', 'they', 'them', 'their', 'theirs', 'themselves',
        'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those',
        'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have',
        'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an',
        'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while',
        'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between',
        'into', 'through', 'during', 'before', 'after', 'above', 'below',
        'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over',
        'under', 'again', 'further', 'then', 'once', 'here', 'there',
        'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each',
        'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor',
        'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
        's', 't', 'can', 'will', 'just', 'don', 'should', 'now', 'app'
      ];
      
      // Extract words, remove stop words, and filter out short words
      const words = text.toLowerCase().match(/\b(\w+)\b/g) || [];
      return words.filter(word => 
        !stopWords.includes(word) && word.length > 3
      );
    }

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/appreply-co/mcp-appstore'

If you have feedback or need assistance with the MCP directory API, please join our Discord server