Skip to main content
Glama
index.ts18.1 kB
/** * MCP Tool implementations */ import { z } from 'zod'; import { RedditAPI } from '../services/reddit-api.js'; import { ContentProcessor } from '../services/content-processor.js'; import { RedditListing, RedditPost } from '../types/reddit.types.js'; // Tool schemas export const browseSubredditSchema = z.object({ subreddit: z.string().describe('Subreddit name without r/ prefix. Use specific subreddit (e.g., "technology"), "all" for Reddit-wide posts, or "popular" for trending across default subreddits'), sort: z.enum(['hot', 'new', 'top', 'rising', 'controversial']).optional().default('hot'), time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).optional(), limit: z.number().min(1).max(100).optional().default(25).describe('Default 25, range (1-100). Change ONLY IF user specifies.'), include_nsfw: z.boolean().optional().default(false), include_subreddit_info: z.boolean().optional().default(false).describe('Include subreddit metadata like subscriber count and description'), }); export const searchRedditSchema = z.object({ query: z.string().describe('Search query'), subreddits: z.array(z.string()).optional().describe('Subreddits to search in (leave empty for all)'), sort: z.enum(['relevance', 'hot', 'top', 'new', 'comments']).optional().default('relevance'), time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).optional().default('all'), limit: z.number().min(1).max(100).optional().default(25).describe('Default 25, range (1-100). Override ONLY IF user requests.'), author: z.string().optional(), flair: z.string().optional(), }); export const getPostDetailsSchema = z.object({ post_id: z.string().optional().describe('Reddit post ID (e.g., "1abc2d3")'), subreddit: z.string().optional().describe('Subreddit name (optional with post_id, but more efficient if provided)'), url: z.string().optional().describe('Full Reddit URL (alternative to post_id)'), comment_limit: z.number().min(1).max(500).optional().default(20).describe('Default 20, range (1-500). Change ONLY IF user asks.'), comment_sort: z.enum(['best', 'top', 'new', 'controversial', 'qa']).optional().default('best'), comment_depth: z.number().min(1).max(10).optional().default(3).describe('Default 3, range (1-10). Override ONLY IF user specifies.'), extract_links: z.boolean().optional().default(false), max_top_comments: z.number().min(1).max(20).optional().default(5).describe('Default 5, range (1-20). Change ONLY IF user requests.'), }); export const userAnalysisSchema = z.object({ username: z.string().describe('Reddit username'), posts_limit: z.number().min(0).max(100).optional().default(10).describe('Default 10, range (0-100). Change ONLY IF user specifies.'), comments_limit: z.number().min(0).max(100).optional().default(10).describe('Default 10, range (0-100). Override ONLY IF user asks.'), time_range: z.enum(['day', 'week', 'month', 'year', 'all']).optional().default('month').describe('Time range for posts/comments (default: month). Note: When set to values other than "all", posts are sorted by top scores within that period. When set to "all", posts are sorted by newest'), top_subreddits_limit: z.number().min(1).max(50).optional().default(10).describe('Default 10, range (1-50). Change ONLY IF user requests.'), }); export const redditExplainSchema = z.object({ term: z.string().describe('Reddit term to explain (e.g., "karma", "cake day", "AMA")'), }); /** * Tool implementations */ export class RedditTools { constructor(private api: RedditAPI) {} async browseSubreddit(params: z.infer<typeof browseSubredditSchema>) { const listing = await this.api.browseSubreddit( params.subreddit, params.sort, { limit: params.limit, time: params.time, } ); // Filter NSFW content unless requested if (!params.include_nsfw) { listing.data.children = listing.data.children.filter( child => !child.data.over_18 ); } // Extract just the essential fields from Reddit's verbose response const posts = listing.data.children.map(child => ({ id: child.data.id, title: child.data.title, author: child.data.author, score: child.data.score, upvote_ratio: child.data.upvote_ratio, num_comments: child.data.num_comments, created_utc: child.data.created_utc, url: child.data.url, permalink: `https://reddit.com${child.data.permalink}`, subreddit: child.data.subreddit, is_video: child.data.is_video, is_text_post: child.data.is_self, content: child.data.selftext?.substring(0, 500), // Limit text preview nsfw: child.data.over_18, stickied: child.data.stickied, link_flair_text: child.data.link_flair_text, })); let result: any = { posts, total_posts: posts.length }; // Optionally fetch subreddit info if (params.include_subreddit_info) { try { const subredditInfo = await this.api.getSubreddit(params.subreddit); result.subreddit_info = { name: subredditInfo.display_name, subscribers: subredditInfo.subscribers, description: subredditInfo.public_description || subredditInfo.description, type: subredditInfo.subreddit_type, created: new Date(subredditInfo.created_utc * 1000), nsfw: subredditInfo.over18, }; } catch (error) { // If subreddit info fails, continue without it console.error(`Failed to fetch subreddit info: ${error}`); } } return result; } async searchReddit(params: z.infer<typeof searchRedditSchema>) { // Handle multiple subreddits let results: RedditListing<RedditPost>; if (params.subreddits && params.subreddits.length > 0) { if (params.subreddits.length === 1) { // Single subreddit - direct search results = await this.api.search(params.query, { subreddit: params.subreddits[0], sort: params.sort, time: params.time, limit: params.limit, }); } else { // Multiple subreddits - parallel search const searchPromises = params.subreddits.map(sub => this.api.search(params.query, { subreddit: sub, sort: params.sort, time: params.time, limit: Math.ceil(params.limit! / params.subreddits!.length), }) ); const allResults = await Promise.all(searchPromises); // Combine results results = { kind: 'Listing', data: { children: allResults.flatMap(r => r.data.children), after: null, before: null, } }; } } else { // Search all of Reddit results = await this.api.search(params.query, { subreddit: undefined, sort: params.sort, time: params.time, limit: params.limit, }); } // Filter by author if specified if (params.author) { results.data.children = results.data.children.filter( child => child.data.author.toLowerCase() === params.author!.toLowerCase() ); } // Filter by flair if specified if (params.flair) { results.data.children = results.data.children.filter( child => child.data.link_flair_text?.toLowerCase().includes(params.flair!.toLowerCase()) ); } // Extract just the essential fields from Reddit's verbose response const posts = results.data.children.map(child => ({ id: child.data.id, title: child.data.title, author: child.data.author, score: child.data.score, upvote_ratio: child.data.upvote_ratio, num_comments: child.data.num_comments, created_utc: child.data.created_utc, url: child.data.url, permalink: `https://reddit.com${child.data.permalink}`, subreddit: child.data.subreddit, is_video: child.data.is_video, is_text_post: child.data.is_self, content: child.data.selftext?.substring(0, 500), // Limit text preview nsfw: child.data.over_18, link_flair_text: child.data.link_flair_text, })); return { results: posts, total_results: posts.length }; } async getPostDetails(params: z.infer<typeof getPostDetailsSchema>) { let postIdentifier: string; if (params.url) { // Extract from URL - returns format "subreddit_postid" postIdentifier = this.extractPostIdFromUrl(params.url); } else if (params.post_id) { // If subreddit provided, use it; otherwise getPost will fetch it postIdentifier = params.subreddit ? `${params.subreddit}_${params.post_id}` : params.post_id; } else { throw new Error('Provide either url OR post_id'); } const [postListing, commentsListing] = await this.api.getPost(postIdentifier, { limit: params.comment_limit, sort: params.comment_sort, depth: params.comment_depth, }); const post = postListing.data.children[0].data; // Extract essential post fields const cleanPost = { id: post.id, title: post.title, author: post.author, score: post.score, upvote_ratio: post.upvote_ratio, num_comments: post.num_comments, created_utc: post.created_utc, url: post.url, permalink: `https://reddit.com${post.permalink}`, subreddit: post.subreddit, is_video: post.is_video, is_text_post: post.is_self, content: post.selftext?.substring(0, 1000), // More text for post details nsfw: post.over_18, link_flair_text: post.link_flair_text, stickied: post.stickied, locked: post.locked, }; // Process comments const comments = commentsListing.data.children .filter(child => child.kind === 't1') // Only comments .map(child => child.data); let result: any = { post: cleanPost, total_comments: comments.length, top_comments: comments.slice(0, params.max_top_comments || 5).map(c => ({ id: c.id, author: c.author, score: c.score, body: (c.body || '').substring(0, 500), created_utc: c.created_utc, depth: c.depth, is_op: c.is_submitter, permalink: `https://reddit.com${c.permalink}`, })), }; // Extract links if requested if (params.extract_links) { const links = new Set<string>(); comments.forEach(c => { const urls = (c.body || '').match(/https?:\/\/[^\s]+/g) || []; urls.forEach(url => links.add(url)); }); result.extracted_links = Array.from(links); } return result; } async userAnalysis(params: z.infer<typeof userAnalysisSchema>) { // Try to get posts within the specified time range first // If time_range is 'all', use 'new' sort; otherwise use 'top' to respect time filtering const sort = params.time_range === 'all' ? 'new' : 'top'; let posts = null; let comments = null; let usedFallback = false; const user = await this.api.getUser(params.username); // Fetch posts if (params.posts_limit > 0) { posts = await this.api.getUserPosts(params.username, 'submitted', { limit: params.posts_limit, sort, time: params.time_range, }); // If no results with time filter, fallback to getting recent posts if (posts.data.children.length === 0 && params.time_range !== 'all') { usedFallback = true; posts = await this.api.getUserPosts(params.username, 'submitted', { limit: params.posts_limit, sort: 'new', time: 'all', }); } } // Fetch comments if (params.comments_limit > 0) { comments = await this.api.getUserPosts(params.username, 'comments', { limit: params.comments_limit, sort, time: params.time_range, }); // If no results with time filter, fallback to getting recent comments if (comments.data.children.length === 0 && params.time_range !== 'all') { usedFallback = true; comments = await this.api.getUserPosts(params.username, 'comments', { limit: params.comments_limit, sort: 'new', time: 'all', }); } } // Process summary let summary: any; if (posts) { summary = ContentProcessor.processUserSummary(user, posts, { maxTopSubreddits: params.top_subreddits_limit, comments: comments || undefined }); } else { // Fallback when no posts - but still include comments if available summary = { username: user.name, accountAge: 'Unknown', karma: { link: user.link_karma, comment: user.comment_karma, total: user.link_karma + user.comment_karma, }, }; // Add comments even when there are no posts if (comments && comments.data.children.length > 0) { summary.recentComments = comments.data.children.map(child => { const comment = child.data as any; return { id: comment.id, body: comment.body?.substring(0, 200) + (comment.body?.length > 200 ? '...' : ''), score: comment.score, subreddit: comment.subreddit || 'unknown', postTitle: comment.link_title, created: new Date(comment.created_utc * 1000), url: `https://reddit.com${comment.permalink}`, }; }); } } // Add note if we used fallback data if (usedFallback && params.time_range !== 'all') { const timeRangeLabel = { 'hour': 'hour', 'day': 'day', 'week': 'week', 'month': 'month', 'year': 'year' }[params.time_range]; summary.timeRangeNote = `No posts found in the last ${timeRangeLabel}, showing all recent posts instead`; } return summary; } async redditExplain(params: z.infer<typeof redditExplainSchema>) { // This would ideally use a knowledge base, but we'll provide common explanations const explanations: Record<string, any> = { 'karma': { definition: 'Reddit points earned from upvotes on posts and comments', origin: 'Concept from Hinduism/Buddhism adapted for Reddit\'s scoring system', usage: 'Users accumulate karma to show contribution quality', examples: ['High karma users are often trusted more', 'Some subreddits require minimum karma to post'], }, 'cake day': { definition: 'Anniversary of when a user joined Reddit', origin: 'Reddit displays a cake icon next to usernames on this day', usage: 'Users often get extra upvotes on their cake day', examples: ['Happy cake day!', 'It\'s my cake day, AMA'], }, 'ama': { definition: 'Ask Me Anything - Q&A session with interesting people', origin: 'Started in r/IAmA subreddit', usage: 'Celebrities, experts, or people with unique experiences answer questions', examples: ['I am Elon Musk, AMA', 'I survived a plane crash, AMA'], }, 'eli5': { definition: 'Explain Like I\'m 5 - request for simple explanation', origin: 'From r/explainlikeimfive subreddit', usage: 'Used when asking for complex topics to be explained simply', examples: ['ELI5: How does bitcoin work?', 'Can someone ELI5 quantum computing?'], }, 'til': { definition: 'Today I Learned - sharing interesting facts', origin: 'From r/todayilearned subreddit', usage: 'Prefix for sharing newly discovered information', examples: ['TIL bananas are berries', 'TIL about the Baader-Meinhof phenomenon'], }, 'op': { definition: 'Original Poster - person who created the post', origin: 'Common internet forum terminology', usage: 'Refers to the person who started the discussion', examples: ['OP delivers!', 'Waiting for OP to respond'], }, 'repost': { definition: 'Content that has been posted before', origin: 'Common issue on content aggregation sites', usage: 'Often called out by users who\'ve seen the content before', examples: ['This is a repost from last week', 'General Reposti!'], }, 'brigading': { definition: 'Coordinated effort to manipulate votes or harass', origin: 'Named after military brigade tactics', usage: 'Against Reddit rules, can result in bans', examples: ['Don\'t brigade other subs', 'This looks like brigading'], }, '/s': { definition: 'Sarcasm indicator', origin: 'HTML-style closing tag for sarcasm', usage: 'Added to end of sarcastic comments to avoid misunderstanding', examples: ['Yeah, that\'s totally going to work /s', 'Great idea /s'], }, 'banana for scale': { definition: 'Using a banana to show size in photos', origin: 'Started as a Reddit meme in 2013', usage: 'Humorous way to provide size reference', examples: ['Found this rock, banana for scale', 'No banana for scale?'], }, }; const term = params.term.toLowerCase(); const explanation = explanations[term]; if (!explanation) { return { definition: 'Term not found in database. This might be a subreddit-specific term or newer slang.', origin: 'Unknown', usage: 'Try searching Reddit for this term to see how it\'s used', examples: [], relatedTerms: [], }; } return { definition: explanation.definition, origin: explanation.origin, usage: explanation.usage, examples: explanation.examples, relatedTerms: [], }; } private extractPostIdFromUrl(url: string): string { const match = url.match(/\/r\/(\w+)\/comments\/(\w+)/); if (match) { return `${match[1]}_${match[2]}`; } throw new Error('Invalid Reddit post URL'); } }

Implementation Reference

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/karanb192/reddit-buddy-mcp'

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