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
| Name | Required | Description | Default |
|---|---|---|---|
| appId | Yes | The unique identifier for the app (Android package name, iOS numeric ID or bundle ID). | |
| platform | Yes | The platform of the app ('ios' or 'android'). | |
| num | No | 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 | No | Two-letter country code for reviews localization. Default 'us'. | us |
| lang | No | Language code for filtering reviews (results may be less accurate if language doesn't match review content). Default 'en'. | en |
| sort | No | Sorting order for reviews: 'newest', 'rating' (highest/lowest first, platform dependent), 'helpfulness'. Default 'newest'. | newest |
Implementation Reference
- server.js:444-803 (handler)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 }; } }
- server.js:436-442 (schema)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 }; } } );
- server.js:538-574 (helper)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'; }
- server.js:577-605 (helper)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 ); }