Skip to main content
Glama

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