analyze_reviews
Analyze Steam game reviews to extract sentiment, identify common themes, and uncover key insights for informed purchasing decisions.
Instructions
Fetch and analyze Steam game reviews to extract sentiment, common themes, and key insights. Supports optional topic drill-down, time-bounded analysis, and pre-fetched reviews.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| appId | Yes | Steam AppID of the game to analyze | |
| sampleSize | No | Number of reviews to analyze (default: 100, max: 200) | |
| language | No | Filter reviews by language (e.g., "english", "schinese") | |
| reviewType | No | Filter by review sentiment (default: all) | |
| topic | No | Optional: Drill down into specific theme (e.g., "performance", "multiplayer") | |
| dayRange | No | Only analyze reviews from last N days (e.g., 30, 90, 365) | |
| filterOfftopicActivity | No | Filter out review bombing (default: false to show all reviews including controversies) | |
| steamDeckOnly | No | Only analyze Steam Deck reviews (experimental) | |
| preFetchedReviews | No | Optional: Pre-fetched reviews to analyze instead of fetching new ones. Useful to avoid duplicate API calls. If provided, sampleSize, language, reviewType, dayRange, and filtering parameters are ignored. |
Implementation Reference
- src/index.ts:646-724 (handler)Main handler for analyze_reviews tool in stdio mode. Validates input, fetches or uses pre-fetched reviews (with pagination support), calls either summarizeReviews or analyzeTopicFocused based on whether a topic is provided, and returns the analysis results.} else if (name === 'analyze_reviews') { const validatedInput = analyzeReviewsSchema.parse(args); let allReviews: import('./types.js').Review[]; if (validatedInput.preFetchedReviews && validatedInput.preFetchedReviews.length > 0) { // Use pre-fetched reviews (type assertion) allReviews = validatedInput.preFetchedReviews as import('./types.js').Review[]; } else { // Fetch reviews as before const sampleSize = validatedInput.sampleSize || 100; // Fetch reviews for analysis const reviewsResponse = await steamClient.getAppReviews(validatedInput.appId, { language: validatedInput.language, reviewType: validatedInput.reviewType, limit: Math.min(sampleSize, 100), // Steam API max per page dayRange: validatedInput.dayRange, filterOfftopicActivity: validatedInput.filterOfftopicActivity, steamDeckOnly: validatedInput.steamDeckOnly, }); allReviews = reviewsResponse.reviews; // Fetch additional pages if needed to reach sample size if (sampleSize > 100 && reviewsResponse.cursor) { const remaining = sampleSize - allReviews.length; const secondPageSize = Math.min(remaining, 100); const page2 = await steamClient.getAppReviews(validatedInput.appId, { language: validatedInput.language, reviewType: validatedInput.reviewType, limit: secondPageSize, cursor: reviewsResponse.cursor, dayRange: validatedInput.dayRange, filterOfftopicActivity: validatedInput.filterOfftopicActivity, steamDeckOnly: validatedInput.steamDeckOnly, }); allReviews = [...allReviews, ...page2.reviews]; } } // Handle case where no reviews were found if (allReviews.length === 0) { return { content: [ { type: 'text', text: JSON.stringify( { error: 'No reviews found', details: 'No reviews were found for the specified game and filters.', }, null, 2 ), }, ], }; } // Analyze reviews - use topic-focused analysis if topic provided // Pass appId to enable example quotes with clickable Steam community links let analysis; if (validatedInput.topic) { analysis = analyzeTopicFocused(allReviews, validatedInput.topic, validatedInput.appId); } else { analysis = summarizeReviews(allReviews, validatedInput.appId); } return { content: [ { type: 'text', text: JSON.stringify(analysis, null, 2), }, ], };
- src/index.ts:967-1029 (handler)Duplicate handler for analyze_reviews tool in HTTP/SSE mode. Contains identical logic to the stdio handler for processing review analysis requests.} else if (name === 'analyze_reviews') { const validatedInput = analyzeReviewsSchema.parse(args); let allReviews: import('./types.js').Review[]; if (validatedInput.preFetchedReviews && validatedInput.preFetchedReviews.length > 0) { allReviews = validatedInput.preFetchedReviews as import('./types.js').Review[]; } else { const sampleSize = validatedInput.sampleSize || 100; const reviewsResponse = await steamClient.getAppReviews(validatedInput.appId, { language: validatedInput.language, reviewType: validatedInput.reviewType, limit: Math.min(sampleSize, 100), dayRange: validatedInput.dayRange, filterOfftopicActivity: validatedInput.filterOfftopicActivity, steamDeckOnly: validatedInput.steamDeckOnly, }); allReviews = reviewsResponse.reviews; if (sampleSize > 100 && reviewsResponse.cursor) { const remaining = sampleSize - allReviews.length; const secondPageSize = Math.min(remaining, 100); const page2 = await steamClient.getAppReviews(validatedInput.appId, { language: validatedInput.language, reviewType: validatedInput.reviewType, limit: secondPageSize, cursor: reviewsResponse.cursor, dayRange: validatedInput.dayRange, filterOfftopicActivity: validatedInput.filterOfftopicActivity, steamDeckOnly: validatedInput.steamDeckOnly, }); allReviews = [...allReviews, ...page2.reviews]; } } if (allReviews.length === 0) { return { content: [ { type: 'text', text: JSON.stringify( { error: 'No reviews found', details: 'No reviews were found for the specified game and filters.', }, null, 2 ), }, ], }; } let analysis; if (validatedInput.topic) { analysis = analyzeTopicFocused(allReviews, validatedInput.topic, validatedInput.appId); } else { analysis = summarizeReviews(allReviews, validatedInput.appId); } return { content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }], };
- src/index.ts:306-316 (schema)Zod validation schema for analyze_reviews input. Validates appId (required), sampleSize (10-200, optional), language, reviewType (enum), topic, dayRange, filterOfftopicActivity, steamDeckOnly, and preFetchedReviews parameters.const analyzeReviewsSchema = z.object({ appId: z.number(), sampleSize: z.number().min(10).max(200).optional(), language: z.string().optional(), reviewType: z.enum(['all', 'positive', 'negative']).optional(), topic: z.string().optional(), dayRange: z.number().min(1).optional(), filterOfftopicActivity: z.boolean().optional(), steamDeckOnly: z.boolean().optional(), preFetchedReviews: z.array(z.any()).optional(), // z.any() since Review type is complex });
- src/index.ts:189-246 (registration)Tool registration for analyze_reviews in the tools array. Defines the tool name, description, and JSON input schema with all available parameters (appId, sampleSize, language, reviewType, topic, dayRange, filterOfftopicActivity, steamDeckOnly, preFetchedReviews).{ name: 'analyze_reviews', description: 'Fetch and analyze Steam game reviews to extract sentiment, common themes, and key insights. Supports optional topic drill-down, time-bounded analysis, and pre-fetched reviews.', inputSchema: { type: 'object', properties: { appId: { type: 'number', description: 'Steam AppID of the game to analyze', }, sampleSize: { type: 'number', description: 'Number of reviews to analyze (default: 100, max: 200)', minimum: 10, maximum: 200, }, language: { type: 'string', description: 'Filter reviews by language (e.g., "english", "schinese")', }, reviewType: { type: 'string', enum: ['all', 'positive', 'negative'], description: 'Filter by review sentiment (default: all)', }, topic: { type: 'string', description: 'Optional: Drill down into specific theme (e.g., "performance", "multiplayer")', }, dayRange: { type: 'number', description: 'Only analyze reviews from last N days (e.g., 30, 90, 365)', minimum: 1, }, filterOfftopicActivity: { type: 'boolean', description: 'Filter out review bombing (default: false to show all reviews including controversies)', }, steamDeckOnly: { type: 'boolean', description: 'Only analyze Steam Deck reviews (experimental)', }, preFetchedReviews: { type: 'array', items: { type: 'object', description: 'Review object from fetch_reviews tool', }, description: 'Optional: Pre-fetched reviews to analyze instead of fetching new ones. Useful to avoid duplicate API calls. If provided, sampleSize, language, reviewType, dayRange, and filtering parameters are ignored.', }, }, required: ['appId'], }, },
- src/utils/analysis.ts:293-358 (helper)Core analysis function summarizeReviews that performs comprehensive review analysis including sentiment analysis, keyword extraction (positive/negative themes), summary text generation, and example quote selection with clickable Steam community URLs.export function summarizeReviews(reviews: Review[], appId?: number): ReviewAnalysis { if (reviews.length === 0) { return { summary: 'No reviews to analyze', sentiment: { score: 0, label: 'neutral', confidence: 0 }, commonThemes: [], positiveKeywords: [], negativeKeywords: [], totalAnalyzed: 0, sampleSize: 0, }; } // Separate positive and negative reviews const positiveReviews = reviews.filter((r) => r.votedUp); const negativeReviews = reviews.filter((r) => !r.votedUp); // Analyze sentiment of each review const sentiments = reviews.map((r) => analyzeSentiment(r.review)); const avgScore = sentiments.reduce((sum, s) => sum + s.score, 0) / sentiments.length; const avgConfidence = sentiments.reduce((sum, s) => sum + s.confidence, 0) / sentiments.length; // Overall sentiment let overallLabel: 'positive' | 'negative' | 'neutral'; if (avgScore > 0.1) overallLabel = 'positive'; else if (avgScore < -0.1) overallLabel = 'negative'; else overallLabel = 'neutral'; // Extract keywords from positive and negative reviews const positiveText = positiveReviews.map((r) => r.review).join(' '); const negativeText = negativeReviews.map((r) => r.review).join(' '); const positiveKeywords = extractKeywords(positiveText, 10); const negativeKeywords = extractKeywords(negativeText, 10); // Combine all keywords for common themes const allText = reviews.map((r) => r.review).join(' '); const commonThemes = extractKeywords(allText, 15); // Generate summary const posPercent = Math.round((positiveReviews.length / reviews.length) * 100); const negPercent = 100 - posPercent; const summary = `Analyzed ${reviews.length} reviews: ${posPercent}% positive, ${negPercent}% negative. ` + `Overall sentiment is ${overallLabel} (score: ${avgScore.toFixed(2)}). ` + `Common themes: ${commonThemes.slice(0, 5).join(', ')}.`; // Select example quotes if appId is provided const exampleQuotes = appId ? selectExampleQuotes(reviews, appId) : undefined; return { summary, sentiment: { score: avgScore, label: overallLabel, confidence: avgConfidence, }, commonThemes, positiveKeywords, negativeKeywords, totalAnalyzed: reviews.length, sampleSize: reviews.length, exampleQuotes, }; }