import crypto from "crypto"
import dotenv from "dotenv"
import { FastMCP } from "fastmcp"
import { z } from "zod"
import { getRedditClient, initializeRedditClient } from "./client/reddit-client"
import type { RedditAuthMode, RedditSafeMode, SafeModeConfig } from "./types"
import { formatPostInfo, formatSubredditInfo, formatUserInfo } from "./utils/formatters"
// Load environment variables
dotenv.config()
// Version injected at build time by tsdown
declare const __VERSION__: string
const VERSION = (typeof __VERSION__ !== "undefined" ? __VERSION__ : "0.0.0-dev") as `${number}.${number}.${number}`
// User-Agent validation and building
function validateUserAgent(userAgent: string, username?: string): void {
const recommendedPattern = /^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/
if (!recommendedPattern.test(userAgent)) {
console.error("[Warning] User-Agent does not follow Reddit's recommended format")
console.error("[Warning] Recommended: 'platform:app_id:version (by /u/username)'")
console.error("[Warning] Non-standard User-Agents may increase ban risk")
if (username) {
console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server:${VERSION} (by /u/${username})'`)
}
}
}
function buildUserAgent(customAgent?: string, username?: string): string {
// If custom agent provided, use it (but validate)
if (customAgent) {
validateUserAgent(customAgent, username)
return customAgent
}
// Auto-format with username if available
if (username) {
const autoAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/${username})`
console.error(`[Setup] Auto-generated User-Agent: ${autoAgent}`)
return autoAgent
}
// Fallback using recommended format without username
const fallbackAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/anonymous)`
console.error(
"[Setup] No REDDIT_USERNAME set — using anonymous User-Agent. Set REDDIT_USERNAME for a personalized agent.",
)
return fallbackAgent
}
// Safe mode configuration
function buildSafeModeConfig(safeMode: RedditSafeMode): SafeModeConfig {
switch (safeMode) {
case "off":
return {
enabled: false,
mode: "off",
writeDelayMs: 0,
duplicateCheck: false,
maxRecentHashes: 10,
}
case "standard":
return {
enabled: true,
mode: "standard",
writeDelayMs: 2000, // 2 seconds between writes
duplicateCheck: true,
maxRecentHashes: 10,
}
case "strict":
return {
enabled: true,
mode: "strict",
writeDelayMs: 5000, // 5 seconds between writes
duplicateCheck: true,
maxRecentHashes: 20,
}
}
}
// Initialize Reddit client
async function setupRedditClient() {
const clientId = process.env.REDDIT_CLIENT_ID
const clientSecret = process.env.REDDIT_CLIENT_SECRET
const customUserAgent = process.env.REDDIT_USER_AGENT
const username = process.env.REDDIT_USERNAME
const password = process.env.REDDIT_PASSWORD
const authMode = (process.env.REDDIT_AUTH_MODE || "auto") as RedditAuthMode
const safeMode = (process.env.REDDIT_SAFE_MODE || "off") as RedditSafeMode
// Validate auth mode
if (!["auto", "authenticated", "anonymous"].includes(authMode)) {
console.error(`[Error] Invalid REDDIT_AUTH_MODE: ${authMode}`)
console.error("[Error] Valid options are: auto, authenticated, anonymous")
process.exit(1)
}
// Validate safe mode
if (!["off", "standard", "strict"].includes(safeMode)) {
console.error(`[Error] Invalid REDDIT_SAFE_MODE: ${safeMode}`)
console.error("[Error] Valid options are: off, standard, strict")
process.exit(1)
}
// In authenticated mode, require credentials
if (authMode === "authenticated" && (!clientId || !clientSecret)) {
console.error("[Error] Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET")
process.exit(1)
}
// For auto/anonymous, credentials are optional
const hasCredentials = !!(clientId && clientSecret)
// Build user-agent (auto-format with username if available)
const userAgent = buildUserAgent(customUserAgent, username)
// Build safe mode config
const safeModeConfig = buildSafeModeConfig(safeMode)
try {
const client = initializeRedditClient({
clientId: clientId || "",
clientSecret: clientSecret || "",
userAgent,
username,
password,
authMode,
safeMode: safeModeConfig,
})
console.error("[Setup] Reddit client initialized")
console.error(`[Setup] Authentication mode: ${authMode}`)
if (authMode === "anonymous" || !hasCredentials) {
console.error("[Setup] Using anonymous Reddit API (~10 req/min)")
console.error("[Setup] No authentication required - ready to use!")
} else {
console.error("[Setup] Testing Reddit API connection...")
const isConnected = await client.checkAuthentication()
if (!isConnected) {
console.error("[Error] ✗ Failed to connect to Reddit API")
console.error("[Error] Please check your REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET")
process.exit(1)
}
console.error("[Setup] ✓ Reddit API connection successful")
console.error("[Setup] Using OAuth Reddit API (60-100 req/min)")
}
if (username && password) {
console.error(`[Setup] ✓ User authenticated as: ${username}`)
console.error("[Setup] Write operations enabled (posting, replying, editing, deleting)")
} else {
console.error("[Setup] Read-only mode (no user credentials)")
console.error("[Setup] For write operations, set REDDIT_USERNAME and REDDIT_PASSWORD")
}
// Log safe mode status
if (safeModeConfig.enabled) {
console.error(`[Setup] ✓ Safe mode enabled: ${safeModeConfig.mode}`)
console.error(`[Setup] - Write delay: ${safeModeConfig.writeDelayMs}ms between operations`)
console.error(`[Setup] - Duplicate detection: enabled (tracking last ${safeModeConfig.maxRecentHashes} items)`)
} else {
console.error("[Setup] Safe mode: off (no write operation safeguards)")
console.error("[Setup] Consider enabling REDDIT_SAFE_MODE=standard for spam protection")
}
} catch (error) {
console.error("[Error] ✗ Reddit API connection failed:", error instanceof Error ? error.message : error)
console.error("[Error] Please verify your Reddit API credentials")
process.exit(1)
}
}
// OAuth token: generate once at startup, never expose in responses
const oauthToken = process.env.OAUTH_TOKEN || crypto.randomBytes(32).toString("hex")
if (process.env.OAUTH_ENABLED === "true" && !process.env.OAUTH_TOKEN) {
console.error(`[Auth] Generated OAuth token: ${oauthToken}`)
}
// Create FastMCP server
const server = new FastMCP({
name: "reddit-mcp-server",
version: VERSION,
instructions: `A comprehensive Reddit MCP server that provides tools for interacting with Reddit API.
Available capabilities:
- Fetch Reddit posts, comments, and user information
- Get subreddit details and statistics
- Search Reddit content across posts and subreddits
- Create posts and reply to posts/comments (with authentication)
- Edit your own posts and comments (with authentication)
- Delete your own posts and comments (with authentication)
- Analyze engagement metrics and community insights
For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
// Optional OAuth configuration for HTTP transport
...(process.env.OAUTH_ENABLED === "true" && {
authenticate: async (request) => {
const authHeader = request.headers.authorization
if (!authHeader?.startsWith("Bearer ")) {
throw new Response(null, {
status: 401,
statusText: "Missing or invalid Authorization header",
})
}
const token = authHeader.slice(7)
const tokenBuffer = Buffer.from(token)
const expectedBuffer = Buffer.from(oauthToken)
// Constant-time comparison: hash both so timingSafeEqual always compares equal-length buffers
const tokenHash = crypto.createHash("sha256").update(tokenBuffer).digest()
const expectedHash = crypto.createHash("sha256").update(expectedBuffer).digest()
if (!crypto.timingSafeEqual(tokenHash, expectedHash)) {
throw new Response(null, {
status: 403,
statusText: "Invalid token",
})
}
return { authenticated: true }
},
}),
})
// Test tool
server.addTool({
name: "test_reddit_mcp_server",
description: "Test the Reddit MCP Server connection and configuration",
parameters: z.object({}),
execute: async () => {
const client = getRedditClient()
const hasAuth = client ? "✓" : "✗"
const hasWriteAccess = process.env.REDDIT_USERNAME && process.env.REDDIT_PASSWORD ? "✓" : "✗"
return `Reddit MCP Server Status:
- Server: ✓ Running
- Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
- Write Access: ${hasWriteAccess} ${hasWriteAccess === "✓" ? "Available" : "Read-only mode"}
- Version: ${VERSION}
Ready to handle Reddit API requests!`
},
})
// User tools
server.addTool({
name: "get_user_info",
description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
parameters: z.object({
username: z.string().describe("The Reddit username (without u/ prefix)"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const user = await client.getUser(args.username)
const formattedUser = formatUserInfo(user)
return `# User Information: u/${formattedUser.username}
## Profile Overview
- Username: u/${formattedUser.username}
- Karma:
- Comment Karma: ${formattedUser.karma.commentKarma.toLocaleString()}
- Post Karma: ${formattedUser.karma.postKarma.toLocaleString()}
- Total Karma: ${formattedUser.karma.totalKarma.toLocaleString()}
- Account Status: ${formattedUser.accountStatus.join(", ")}
- Account Created: ${formattedUser.accountCreated}
- Profile URL: ${formattedUser.profileUrl}
## Activity Analysis
- ${formattedUser.activityAnalysis.replace(/\n {2}- /g, "\n- ")}
## Recommendations
- ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`
},
})
server.addTool({
name: "get_user_posts",
description: "Get recent posts by a Reddit user with sorting and filtering options",
parameters: z.object({
username: z.string().describe("The Reddit username (without u/ prefix)"),
sort: z.enum(["new", "hot", "top"]).default("new").describe("Sort order for posts"),
time_filter: z
.enum(["hour", "day", "week", "month", "year", "all"])
.default("all")
.describe("Time filter for top posts"),
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const posts = await client.getUserPosts(args.username, {
sort: args.sort,
timeFilter: args.time_filter,
limit: args.limit,
})
if (posts.length === 0) {
return `No posts found for u/${args.username} with the specified filters.`
}
const postSummaries = posts
.map((post, index) => {
const flags = [...(post.over18 ? ["**NSFW**"] : []), ...(post.spoiler ? ["**Spoiler**"] : [])]
return `### ${index + 1}. ${post.title} ${flags.join(" ")}
- Subreddit: r/${post.subreddit}
- Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
- Comments: ${post.numComments.toLocaleString()}
- Posted: ${new Date(post.createdUtc * 1000).toLocaleString()}
- Link: https://reddit.com${post.permalink}`
})
.join("\n\n")
return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
${postSummaries}`
},
})
server.addTool({
name: "get_user_comments",
description: "Get recent comments by a Reddit user with sorting and filtering options",
parameters: z.object({
username: z.string().describe("The Reddit username (without u/ prefix)"),
sort: z.enum(["new", "hot", "top"]).default("new").describe("Sort order for comments"),
time_filter: z
.enum(["hour", "day", "week", "month", "year", "all"])
.default("all")
.describe("Time filter for top comments"),
limit: z.number().min(1).max(100).default(10).describe("Number of comments to retrieve"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const comments = await client.getUserComments(args.username, {
sort: args.sort,
timeFilter: args.time_filter,
limit: args.limit,
})
if (comments.length === 0) {
return `No comments found for u/${args.username} with the specified filters.`
}
const commentSummaries = comments
.map((comment, index) => {
const truncatedBody = comment.body.length > 300 ? `${comment.body.substring(0, 300)}...` : comment.body
const flags = [...(comment.edited ? ["*(edited)*"] : []), ...(comment.isSubmitter ? ["**OP**"] : [])]
return `### ${index + 1}. Comment ${flags.join(" ")}
In r/${comment.subreddit} on "${comment.submissionTitle}"
> ${truncatedBody}
- Score: ${comment.score.toLocaleString()}
- Posted: ${new Date(comment.createdUtc * 1000).toLocaleString()}
- Link: https://reddit.com${comment.permalink}`
})
.join("\n\n")
return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
${commentSummaries}`
},
})
// Post tools
server.addTool({
name: "get_reddit_post",
description:
"Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
parameters: z.object({
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
post_id: z.string().describe("The Reddit post ID"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const post = await client.getPost(args.post_id, args.subreddit)
const formattedPost = formatPostInfo(post)
return `# Post from r/${formattedPost.subreddit}
## Post Details
- Title: ${formattedPost.title}
- Type: ${formattedPost.type}
- Author: u/${formattedPost.author}
## Content
${formattedPost.content}
## Stats
- Score: ${formattedPost.stats.score.toLocaleString()}
- Upvote Ratio: ${(formattedPost.stats.upvoteRatio * 100).toFixed(1)}%
- Comments: ${formattedPost.stats.comments.toLocaleString()}
## Metadata
- Posted: ${formattedPost.metadata.posted}
- Flags: ${formattedPost.metadata.flags.length ? formattedPost.metadata.flags.join(", ") : "None"}
- Flair: ${formattedPost.metadata.flair}
## Links
- Full Post: ${formattedPost.links.fullPost}
- Short Link: ${formattedPost.links.shortLink}
## Engagement Analysis
- ${formattedPost.engagementAnalysis.replace(/\n {2}- /g, "\n- ")}
## Best Time to Engage
${formattedPost.bestTimeToEngage}`
},
})
server.addTool({
name: "get_top_posts",
description: "Get top posts from a subreddit or from the Reddit home feed",
parameters: z.object({
subreddit: z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
time_filter: z
.enum(["hour", "day", "week", "month", "year", "all"])
.default("week")
.describe("Time period for top posts"),
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const posts = await client.getTopPosts(args.subreddit || "", args.time_filter, args.limit)
if (posts.length === 0) {
const location = args.subreddit ? `r/${args.subreddit}` : "home feed"
return `No posts found in ${location} for the specified time period.`
}
const formattedPosts = posts.map(formatPostInfo)
const postSummaries = formattedPosts
.map(
(post, index) => `### ${index + 1}. ${post.title}
- Author: u/${post.author}
- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
- Comments: ${post.stats.comments.toLocaleString()}
- Posted: ${post.metadata.posted}
- Link: ${post.links.shortLink}`,
)
.join("\n\n")
const location = args.subreddit ? `r/${args.subreddit}` : "Home Feed"
return `# Top Posts from ${location} (${args.time_filter})
${postSummaries}`
},
})
// Subreddit tools
server.addTool({
name: "get_subreddit_info",
description: "Get detailed information about a subreddit including description, stats, and community analysis",
parameters: z.object({
subreddit_name: z.string().describe("The subreddit name (without r/ prefix)"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const subreddit = await client.getSubredditInfo(args.subreddit_name)
const formattedSubreddit = formatSubredditInfo(subreddit)
return `# Subreddit Information: r/${formattedSubreddit.name}
## Overview
- Name: r/${formattedSubreddit.name}
- Title: ${formattedSubreddit.title}
- Subscribers: ${formattedSubreddit.stats.subscribers.toLocaleString()}
- Active Users: ${
typeof formattedSubreddit.stats.activeUsers === "number"
? formattedSubreddit.stats.activeUsers.toLocaleString()
: formattedSubreddit.stats.activeUsers
}
## Description
${formattedSubreddit.description.short}
## Detailed Description
${formattedSubreddit.description.full}
## Metadata
- Created: ${formattedSubreddit.metadata.created}
- Flags: ${formattedSubreddit.metadata.flags.join(", ")}
## Links
- Subreddit: ${formattedSubreddit.links.subreddit}
- Wiki: ${formattedSubreddit.links.wiki}
## Community Analysis
- ${formattedSubreddit.communityAnalysis.replace(/\n {2}- /g, "\n- ")}
## Engagement Tips
- ${formattedSubreddit.engagementTips.replace(/\n {2}- /g, "\n- ")}`
},
})
server.addTool({
name: "get_trending_subreddits",
description: "Get a list of currently trending subreddits",
parameters: z.object({}),
execute: async () => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
const trendingSubreddits = await client.getTrendingSubreddits()
return `# Trending Subreddits
${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`
},
})
// Search tools
server.addTool({
name: "search_reddit",
description: "Search Reddit for posts and content across subreddits",
parameters: z.object({
query: z.string().describe("Search query"),
subreddit: z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
sort: z.enum(["relevance", "hot", "top", "new", "comments"]).default("relevance").describe("Sort order"),
time_filter: z.enum(["hour", "day", "week", "month", "year", "all"]).default("all").describe("Time filter"),
limit: z.number().min(1).max(100).default(10).describe("Number of results"),
type: z.enum(["link", "sr", "user"]).default("link").describe("Type of content to search"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
if (!args.query || args.query.trim() === "") {
throw new Error("Search query cannot be empty")
}
const posts = await client.searchReddit(args.query, {
subreddit: args.subreddit,
sort: args.sort,
timeFilter: args.time_filter,
limit: args.limit,
type: args.type,
})
if (posts.length === 0) {
const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : ""
return `No results found for "${args.query}"${searchLocation}.`
}
const searchResults = posts
.map((post, index) => {
const flags = [...(post.over18 ? ["**NSFW**"] : []), ...(post.spoiler ? ["**Spoiler**"] : [])]
return `### ${index + 1}. ${post.title} ${flags.join(" ")}
- Subreddit: r/${post.subreddit}
- Author: u/${post.author}
- Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
- Comments: ${post.numComments.toLocaleString()}
- Posted: ${new Date(post.createdUtc * 1000).toLocaleString()}
- Link: https://reddit.com${post.permalink}`
})
.join("\n\n")
const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : ""
return `# Reddit Search Results for: "${args.query}"${searchLocation}
Sorted by: ${args.sort} | Time: ${args.time_filter} | Type: ${args.type}
${searchResults}`
},
})
// Write tools (require user authentication)
server.addTool({
name: "create_post",
description:
"Create a new post in a subreddit (requires REDDIT_USERNAME and REDDIT_PASSWORD). " +
"WARNING: Rapid posting or duplicate content may trigger Reddit's spam detection and result in account bans. " +
"Consider enabling REDDIT_SAFE_MODE=standard for protection.",
parameters: z.object({
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
title: z.string().describe("The post title"),
content: z.string().describe("The post content (text for self posts, URL for link posts)"),
is_self: z.boolean().default(true).describe("Whether this is a self post (text) or link post"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
const post = await client.createPost(args.subreddit, args.title, args.content, args.is_self)
const formattedPost = formatPostInfo(post)
return `# Post Created Successfully
## Post Details
- Title: ${formattedPost.title}
- Subreddit: r/${formattedPost.subreddit}
- Type: ${formattedPost.type}
- Link: ${formattedPost.links.fullPost}
Your post has been successfully submitted to r/${formattedPost.subreddit}.`
},
})
server.addTool({
name: "reply_to_post",
description:
"Post a reply to an existing Reddit post or comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). " +
"WARNING: Rapid commenting or duplicate content may trigger Reddit's spam detection. " +
"Enable REDDIT_SAFE_MODE=standard for rate limiting and duplicate detection.",
parameters: z.object({
post_id: z.string().describe("The Reddit post ID (thing_id, e.g., t3_xxxxx for posts, t1_xxxxx for comments)"),
content: z.string().describe("The reply content"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
const comment = await client.replyToPost(args.post_id, args.content)
return `# Reply Posted Successfully
## Comment Details
- Posted to: ${args.post_id}
- Author: u/${process.env.REDDIT_USERNAME}
- Comment ID: ${comment.id}
Your reply has been successfully posted.`
},
})
server.addTool({
name: "delete_post",
description:
"Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
parameters: z.object({
thing_id: z
.string()
.describe(
"The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing.",
),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
await client.deletePost(args.thing_id)
return `# Post Deleted Successfully
The post ${args.thing_id} has been permanently deleted from Reddit.
**Note**: This action cannot be undone. The post content has been removed and cannot be recovered.`
},
})
server.addTool({
name: "delete_comment",
description:
"Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
parameters: z.object({
thing_id: z
.string()
.describe(
"The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing.",
),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
await client.deleteComment(args.thing_id)
return `# Comment Deleted Successfully
The comment ${args.thing_id} has been permanently deleted from Reddit.
**Note**: This action cannot be undone. The comment content has been removed and cannot be recovered.`
},
})
server.addTool({
name: "edit_post",
description:
"Edit your own Reddit post (self-text posts only, requires REDDIT_USERNAME and REDDIT_PASSWORD). " +
"You can only edit the text content of self posts, not titles or link posts. " +
"WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
parameters: z.object({
thing_id: z
.string()
.describe(
"The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing.",
),
new_text: z.string().describe("The new text content for the post. Supports Reddit markdown formatting."),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
await client.editPost(args.thing_id, args.new_text)
return `# Post Edited Successfully
The post ${args.thing_id} has been updated with your new content.
**Note**:
- Only self (text) posts can be edited
- Post titles cannot be edited
- Link posts cannot be edited
- An "edited" marker will appear on your post`
},
})
server.addTool({
name: "edit_comment",
description:
"Edit your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). " +
"Update the text content of a comment you previously posted. " +
"WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
parameters: z.object({
thing_id: z
.string()
.describe(
"The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing.",
),
new_text: z.string().describe("The new text content for the comment. Supports Reddit markdown formatting."),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
// Check if user credentials are configured
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
throw new Error(
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.",
)
}
await client.editComment(args.thing_id, args.new_text)
return `# Comment Edited Successfully
The comment ${args.thing_id} has been updated with your new content.
**Note**: An "edited" marker will appear on your comment to show it has been modified.`
},
})
// Comment tools
server.addTool({
name: "get_post_comments",
description: "Get comments from a specific Reddit post",
parameters: z.object({
post_id: z.string().describe("The Reddit post ID"),
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
sort: z.enum(["best", "top", "new", "controversial", "old", "qa"]).default("best").describe("Comment sort order"),
limit: z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve"),
}),
execute: async (args) => {
const client = getRedditClient()
if (!client) {
throw new Error("Reddit client not initialized")
}
if (!args.post_id || !args.subreddit) {
throw new Error("post_id and subreddit are required")
}
const data = await client.getPostComments(args.post_id, args.subreddit, {
sort: args.sort,
limit: args.limit,
})
const { post } = data
const { comments } = data
let response = `# Comments for: ${post.title}
**Post by u/${post.author} in r/${post.subreddit}**
- Score: ${post.score.toLocaleString()} | Comments: ${post.numComments.toLocaleString()}
- Posted: ${new Date(post.createdUtc * 1000).toLocaleString()}
---
`
if (comments.length === 0) {
response += "No comments found for this post."
return response
}
const commentSummaries = comments
.map((comment) => {
const indent = "└─".repeat(Math.min(comment.depth || 0, 3))
const authorBadge = comment.isSubmitter ? " **[OP]**" : ""
const editedBadge = comment.edited ? " *(edited)*" : ""
return `${indent} **u/${comment.author}**${authorBadge}${editedBadge} (${comment.score.toLocaleString()} points)
${comment.body}
---`
})
.join("\n\n")
response += commentSummaries
return response
},
})
// Initialize and start server
async function main() {
try {
await setupRedditClient()
// Default to stdio for local/MCP client usage
// Use HTTP only when explicitly requested (e.g., for Docker)
const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http"
const port = parseInt(process.env.PORT || "3000")
const host = process.env.HOST || "127.0.0.1"
if (useHttp) {
console.error(`[Setup] Starting HTTP server on ${host}:${port}`)
await server.start({
transportType: "httpStream",
httpStream: {
port,
host,
endpoint: "/mcp",
},
})
console.error(`[Setup] HTTP server ready at http://${host}:${port}/mcp`)
console.error(`[Setup] SSE endpoint available at http://${host}:${port}/sse`)
} else {
console.error("[Setup] Starting in stdio mode")
await server.start({
transportType: "stdio",
})
}
} catch (error) {
console.error("[Error] Failed to start server:", error)
process.exit(1)
}
}
// Handle graceful shutdown
process.on("SIGINT", async () => {
console.error("[Shutdown] Shutting down Reddit MCP Server...")
process.exit(0)
})
process.on("SIGTERM", async () => {
console.error("[Shutdown] Shutting down Reddit MCP Server...")
process.exit(0)
})
main().catch(console.error)