reddit-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +18 -0
- package/.env.example +7 -0
- package/.prettierignore +7 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +17 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1200 -0
- package/eslint.config.js +74 -0
- package/package.json +44 -0
- package/pnpm-workspace.yaml +2 -0
- package/smithery.yaml +37 -0
- package/src/client/reddit-client.ts +340 -0
- package/src/index.ts +303 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/post-tools.ts +178 -0
- package/src/tools/subreddit-tools.ts +91 -0
- package/src/tools/user-tools.ts +48 -0
- package/src/types.ts +146 -0
- package/src/utils/formatters.ts +299 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +12 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export interface RedditClientConfig {
|
|
2
|
+
clientId: string
|
|
3
|
+
clientSecret: string
|
|
4
|
+
userAgent: string
|
|
5
|
+
username?: string
|
|
6
|
+
password?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RedditUser {
|
|
10
|
+
name: string
|
|
11
|
+
id: string
|
|
12
|
+
commentKarma: number
|
|
13
|
+
linkKarma: number
|
|
14
|
+
totalKarma: number
|
|
15
|
+
isMod: boolean
|
|
16
|
+
isGold: boolean
|
|
17
|
+
isEmployee: boolean
|
|
18
|
+
createdUtc: number
|
|
19
|
+
profileUrl: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RedditPost {
|
|
23
|
+
id: string
|
|
24
|
+
title: string
|
|
25
|
+
author: string
|
|
26
|
+
subreddit: string
|
|
27
|
+
selftext?: string
|
|
28
|
+
url?: string
|
|
29
|
+
score: number
|
|
30
|
+
upvoteRatio: number
|
|
31
|
+
numComments: number
|
|
32
|
+
createdUtc: number
|
|
33
|
+
over18: boolean
|
|
34
|
+
spoiler?: boolean
|
|
35
|
+
edited: boolean
|
|
36
|
+
isSelf: boolean
|
|
37
|
+
linkFlairText?: string
|
|
38
|
+
permalink: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RedditComment {
|
|
42
|
+
id: string
|
|
43
|
+
author: string
|
|
44
|
+
body: string
|
|
45
|
+
score: number
|
|
46
|
+
controversiality: number
|
|
47
|
+
subreddit: string
|
|
48
|
+
submissionTitle: string
|
|
49
|
+
createdUtc: number
|
|
50
|
+
edited: boolean
|
|
51
|
+
isSubmitter: boolean
|
|
52
|
+
permalink: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RedditSubreddit {
|
|
56
|
+
displayName: string
|
|
57
|
+
title: string
|
|
58
|
+
description: string
|
|
59
|
+
publicDescription: string
|
|
60
|
+
subscribers: number
|
|
61
|
+
activeUserCount?: number
|
|
62
|
+
createdUtc: number
|
|
63
|
+
over18: boolean
|
|
64
|
+
subredditType?: string
|
|
65
|
+
url: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface FormattedUserInfo {
|
|
69
|
+
username: string
|
|
70
|
+
karma: {
|
|
71
|
+
commentKarma: number
|
|
72
|
+
postKarma: number
|
|
73
|
+
totalKarma: number
|
|
74
|
+
}
|
|
75
|
+
accountStatus: string[]
|
|
76
|
+
accountCreated: string
|
|
77
|
+
profileUrl: string
|
|
78
|
+
activityAnalysis: string
|
|
79
|
+
recommendations: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FormattedPostInfo {
|
|
83
|
+
title: string
|
|
84
|
+
type: string
|
|
85
|
+
content: string
|
|
86
|
+
author: string
|
|
87
|
+
subreddit: string
|
|
88
|
+
stats: {
|
|
89
|
+
score: number
|
|
90
|
+
upvoteRatio: number
|
|
91
|
+
comments: number
|
|
92
|
+
}
|
|
93
|
+
metadata: {
|
|
94
|
+
posted: string
|
|
95
|
+
flags: string[]
|
|
96
|
+
flair: string
|
|
97
|
+
}
|
|
98
|
+
links: {
|
|
99
|
+
fullPost: string
|
|
100
|
+
shortLink: string
|
|
101
|
+
}
|
|
102
|
+
engagementAnalysis: string
|
|
103
|
+
bestTimeToEngage: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FormattedSubredditInfo {
|
|
107
|
+
name: string
|
|
108
|
+
title: string
|
|
109
|
+
stats: {
|
|
110
|
+
subscribers: number
|
|
111
|
+
activeUsers: number | string
|
|
112
|
+
}
|
|
113
|
+
description: {
|
|
114
|
+
short: string
|
|
115
|
+
full: string
|
|
116
|
+
}
|
|
117
|
+
metadata: {
|
|
118
|
+
created: string
|
|
119
|
+
flags: string[]
|
|
120
|
+
}
|
|
121
|
+
links: {
|
|
122
|
+
subreddit: string
|
|
123
|
+
wiki: string
|
|
124
|
+
}
|
|
125
|
+
communityAnalysis: string
|
|
126
|
+
engagementTips: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface FormattedCommentInfo {
|
|
130
|
+
author: string
|
|
131
|
+
content: string
|
|
132
|
+
stats: {
|
|
133
|
+
score: number
|
|
134
|
+
controversiality: number | string
|
|
135
|
+
}
|
|
136
|
+
context: {
|
|
137
|
+
subreddit: string
|
|
138
|
+
thread: string
|
|
139
|
+
}
|
|
140
|
+
metadata: {
|
|
141
|
+
posted: string
|
|
142
|
+
flags: string[]
|
|
143
|
+
}
|
|
144
|
+
link: string
|
|
145
|
+
commentAnalysis: string
|
|
146
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RedditUser,
|
|
3
|
+
RedditPost,
|
|
4
|
+
RedditSubreddit,
|
|
5
|
+
RedditComment,
|
|
6
|
+
FormattedUserInfo,
|
|
7
|
+
FormattedPostInfo,
|
|
8
|
+
FormattedSubredditInfo,
|
|
9
|
+
FormattedCommentInfo,
|
|
10
|
+
} from "../types"
|
|
11
|
+
|
|
12
|
+
export function formatTimestamp(timestamp: number): string {
|
|
13
|
+
try {
|
|
14
|
+
const date = new Date(timestamp * 1000)
|
|
15
|
+
return date
|
|
16
|
+
.toISOString()
|
|
17
|
+
.replace("T", " ")
|
|
18
|
+
.replace(/\.\d+Z$/, " UTC")
|
|
19
|
+
} catch {
|
|
20
|
+
return String(timestamp)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function analyzeUserActivity(karmaRatio: number, isMod: boolean, accountAgeDays: number): string {
|
|
25
|
+
const insights: string[] = []
|
|
26
|
+
|
|
27
|
+
// Analyze karma ratio
|
|
28
|
+
if (karmaRatio > 5) {
|
|
29
|
+
insights.push("Primarily a commenter, highly engaged in discussions")
|
|
30
|
+
} else if (karmaRatio < 0.2) {
|
|
31
|
+
insights.push("Content creator, focuses on sharing posts")
|
|
32
|
+
} else {
|
|
33
|
+
insights.push("Balanced participation in both posting and commenting")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Analyze account age and status
|
|
37
|
+
if (accountAgeDays < 30) {
|
|
38
|
+
insights.push("New user, still exploring Reddit")
|
|
39
|
+
} else if (accountAgeDays > 365 * 5) {
|
|
40
|
+
insights.push("Long-time Redditor with extensive platform experience")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isMod) {
|
|
44
|
+
insights.push("Community leader who helps maintain subreddit quality")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return insights.join("\n - ")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function analyzePostEngagement(score: number, ratio: number, numComments: number): string {
|
|
51
|
+
const insights: string[] = []
|
|
52
|
+
|
|
53
|
+
// Analyze score and ratio
|
|
54
|
+
if (score > 1000 && ratio > 0.95) {
|
|
55
|
+
insights.push("Highly successful post with strong community approval")
|
|
56
|
+
} else if (score > 100 && ratio > 0.8) {
|
|
57
|
+
insights.push("Well-received post with good engagement")
|
|
58
|
+
} else if (ratio < 0.5) {
|
|
59
|
+
insights.push("Controversial post that sparked debate")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Analyze comment activity
|
|
63
|
+
if (numComments > 100) {
|
|
64
|
+
insights.push("Generated significant discussion")
|
|
65
|
+
} else if (numComments > score * 0.5) {
|
|
66
|
+
insights.push("Highly discussable content with active comment section")
|
|
67
|
+
} else if (numComments === 0) {
|
|
68
|
+
insights.push("Yet to receive community interaction")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return insights.join("\n - ")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function analyzeSubredditHealth(subscribers: number, activeUsers: number | undefined, ageDays: number): string {
|
|
75
|
+
const insights: string[] = []
|
|
76
|
+
|
|
77
|
+
// Analyze size and activity
|
|
78
|
+
if (subscribers > 1000000) {
|
|
79
|
+
insights.push("Major subreddit with massive following")
|
|
80
|
+
} else if (subscribers > 100000) {
|
|
81
|
+
insights.push("Well-established community")
|
|
82
|
+
} else if (subscribers < 1000) {
|
|
83
|
+
insights.push("Niche community, potential for growth")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (activeUsers) {
|
|
87
|
+
// If we have active users data
|
|
88
|
+
const activityRatio = activeUsers / subscribers
|
|
89
|
+
if (activityRatio > 0.1) {
|
|
90
|
+
insights.push("Highly active community with strong engagement")
|
|
91
|
+
} else if (activityRatio < 0.01) {
|
|
92
|
+
insights.push("Could benefit from more community engagement initiatives")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Analyze age and maturity
|
|
97
|
+
if (ageDays > 365 * 5) {
|
|
98
|
+
insights.push("Mature subreddit with established culture")
|
|
99
|
+
} else if (ageDays < 90) {
|
|
100
|
+
insights.push("New subreddit still forming its community")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return insights.join("\n - ")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getUserRecommendations(karmaRatio: number, isMod: boolean, accountAgeDays: number): string {
|
|
107
|
+
const recommendations: string[] = []
|
|
108
|
+
|
|
109
|
+
if (karmaRatio > 5) {
|
|
110
|
+
recommendations.push("Consider creating more posts to share your expertise")
|
|
111
|
+
} else if (karmaRatio < 0.2) {
|
|
112
|
+
recommendations.push("Engage more in discussions to build community connections")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (accountAgeDays < 30) {
|
|
116
|
+
recommendations.push("Explore popular subreddits in your areas of interest")
|
|
117
|
+
recommendations.push("Read community guidelines before posting")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isMod) {
|
|
121
|
+
recommendations.push("Share moderation insights with other community leaders")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!recommendations.length) {
|
|
125
|
+
recommendations.push("Maintain your balanced engagement across Reddit")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return recommendations.join("\n - ")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getBestEngagementTime(createdUtc: number): string {
|
|
132
|
+
const postHour = new Date(createdUtc * 1000).getHours()
|
|
133
|
+
|
|
134
|
+
// Simple time zone analysis
|
|
135
|
+
if (14 <= postHour && postHour <= 18) {
|
|
136
|
+
// Peak Reddit hours
|
|
137
|
+
return "Posted during peak engagement hours (2 PM - 6 PM), good timing!"
|
|
138
|
+
} else if (23 <= postHour || postHour <= 5) {
|
|
139
|
+
return "Consider posting during more active hours (morning to evening)"
|
|
140
|
+
} else {
|
|
141
|
+
return "Posted during moderate activity hours, timing could be optimized"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getSubredditEngagementTips(subreddit: RedditSubreddit): string {
|
|
146
|
+
const tips: string[] = []
|
|
147
|
+
|
|
148
|
+
if (subreddit.subscribers > 1000000) {
|
|
149
|
+
tips.push("Post during peak hours for maximum visibility")
|
|
150
|
+
tips.push("Ensure content is highly polished due to high competition")
|
|
151
|
+
} else if (subreddit.subscribers < 1000) {
|
|
152
|
+
tips.push("Engage actively to help grow the community")
|
|
153
|
+
tips.push("Consider cross-posting to related larger subreddits")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (subreddit.activeUserCount) {
|
|
157
|
+
const activityRatio = subreddit.activeUserCount / subreddit.subscribers
|
|
158
|
+
if (activityRatio > 0.1) {
|
|
159
|
+
tips.push("Quick responses recommended due to high activity")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return tips.length ? tips.join("\n - ") : "Regular engagement recommended to maintain community presence"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function analyzeCommentImpact(score: number, isEdited: boolean, isOp: boolean): string {
|
|
167
|
+
const insights: string[] = []
|
|
168
|
+
|
|
169
|
+
if (score > 100) {
|
|
170
|
+
insights.push("Highly upvoted comment with significant community agreement")
|
|
171
|
+
} else if (score < 0) {
|
|
172
|
+
insights.push("Controversial or contested viewpoint")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isEdited) {
|
|
176
|
+
insights.push("Refined for clarity or accuracy")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isOp) {
|
|
180
|
+
insights.push("Author's perspective adds context to original post")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return insights.length ? insights.join("\n - ") : "Standard engagement with discussion"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatUserInfo(user: RedditUser): FormattedUserInfo {
|
|
187
|
+
const status: string[] = []
|
|
188
|
+
if (user.isMod) status.push("Moderator")
|
|
189
|
+
if (user.isGold) status.push("Reddit Gold Member")
|
|
190
|
+
if (user.isEmployee) status.push("Reddit Employee")
|
|
191
|
+
|
|
192
|
+
const accountAgeDays = (Date.now() / 1000 - user.createdUtc) / (24 * 3600) // age in days
|
|
193
|
+
const karmaRatio = user.commentKarma / (user.linkKarma || 1)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
username: user.name,
|
|
197
|
+
karma: {
|
|
198
|
+
commentKarma: user.commentKarma,
|
|
199
|
+
postKarma: user.linkKarma,
|
|
200
|
+
totalKarma: user.totalKarma,
|
|
201
|
+
},
|
|
202
|
+
accountStatus: status.length ? status : ["Regular User"],
|
|
203
|
+
accountCreated: formatTimestamp(user.createdUtc),
|
|
204
|
+
profileUrl: user.profileUrl,
|
|
205
|
+
activityAnalysis: analyzeUserActivity(karmaRatio, user.isMod, accountAgeDays),
|
|
206
|
+
recommendations: getUserRecommendations(karmaRatio, user.isMod, accountAgeDays),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function formatPostInfo(post: RedditPost): FormattedPostInfo {
|
|
211
|
+
const contentType = post.isSelf ? "Text Post" : "Link Post"
|
|
212
|
+
const content = post.isSelf ? post.selftext || "" : post.url || ""
|
|
213
|
+
|
|
214
|
+
const flags: string[] = []
|
|
215
|
+
if (post.over18) flags.push("NSFW")
|
|
216
|
+
if (post.spoiler) flags.push("Spoiler")
|
|
217
|
+
if (post.edited) flags.push("Edited")
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
title: post.title,
|
|
221
|
+
type: contentType,
|
|
222
|
+
content: content.length > 300 ? content.substring(0, 297) + "..." : content,
|
|
223
|
+
author: post.author,
|
|
224
|
+
subreddit: post.subreddit,
|
|
225
|
+
stats: {
|
|
226
|
+
score: post.score,
|
|
227
|
+
upvoteRatio: post.upvoteRatio,
|
|
228
|
+
comments: post.numComments,
|
|
229
|
+
},
|
|
230
|
+
metadata: {
|
|
231
|
+
posted: formatTimestamp(post.createdUtc),
|
|
232
|
+
flags: flags,
|
|
233
|
+
flair: post.linkFlairText || "None",
|
|
234
|
+
},
|
|
235
|
+
links: {
|
|
236
|
+
fullPost: `https://reddit.com${post.permalink}`,
|
|
237
|
+
shortLink: `https://redd.it/${post.id}`,
|
|
238
|
+
},
|
|
239
|
+
engagementAnalysis: analyzePostEngagement(post.score, post.upvoteRatio, post.numComments),
|
|
240
|
+
bestTimeToEngage: getBestEngagementTime(post.createdUtc),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function formatSubredditInfo(subreddit: RedditSubreddit): FormattedSubredditInfo {
|
|
245
|
+
const flags: string[] = []
|
|
246
|
+
if (subreddit.over18) flags.push("NSFW")
|
|
247
|
+
if (subreddit.subredditType) flags.push(`Type: ${subreddit.subredditType}`)
|
|
248
|
+
|
|
249
|
+
const ageDays = (Date.now() / 1000 - subreddit.createdUtc) / (24 * 3600)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
name: subreddit.displayName,
|
|
253
|
+
title: subreddit.title,
|
|
254
|
+
stats: {
|
|
255
|
+
subscribers: subreddit.subscribers,
|
|
256
|
+
activeUsers: subreddit.activeUserCount !== undefined ? subreddit.activeUserCount : "Unknown",
|
|
257
|
+
},
|
|
258
|
+
description: {
|
|
259
|
+
short: subreddit.publicDescription,
|
|
260
|
+
full:
|
|
261
|
+
subreddit.description.length > 300 ? subreddit.description.substring(0, 297) + "..." : subreddit.description,
|
|
262
|
+
},
|
|
263
|
+
metadata: {
|
|
264
|
+
created: formatTimestamp(subreddit.createdUtc),
|
|
265
|
+
flags: flags.length ? flags : ["None"],
|
|
266
|
+
},
|
|
267
|
+
links: {
|
|
268
|
+
subreddit: `https://reddit.com${subreddit.url}`,
|
|
269
|
+
wiki: `https://reddit.com/r/${subreddit.displayName}/wiki`,
|
|
270
|
+
},
|
|
271
|
+
communityAnalysis: analyzeSubredditHealth(subreddit.subscribers, subreddit.activeUserCount, ageDays),
|
|
272
|
+
engagementTips: getSubredditEngagementTips(subreddit),
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function formatCommentInfo(comment: RedditComment): FormattedCommentInfo {
|
|
277
|
+
const flags: string[] = []
|
|
278
|
+
if (comment.edited) flags.push("Edited")
|
|
279
|
+
if (comment.isSubmitter) flags.push("OP")
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
author: comment.author,
|
|
283
|
+
content: comment.body.length > 300 ? comment.body.substring(0, 297) + "..." : comment.body,
|
|
284
|
+
stats: {
|
|
285
|
+
score: comment.score,
|
|
286
|
+
controversiality: comment.controversiality,
|
|
287
|
+
},
|
|
288
|
+
context: {
|
|
289
|
+
subreddit: comment.subreddit,
|
|
290
|
+
thread: comment.submissionTitle,
|
|
291
|
+
},
|
|
292
|
+
metadata: {
|
|
293
|
+
posted: formatTimestamp(comment.createdUtc),
|
|
294
|
+
flags: flags.length ? flags : ["None"],
|
|
295
|
+
},
|
|
296
|
+
link: `https://reddit.com${comment.permalink}`,
|
|
297
|
+
commentAnalysis: analyzeCommentImpact(comment.score, comment.edited, comment.isSubmitter),
|
|
298
|
+
}
|
|
299
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./build",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
],
|
|
16
|
+
"exclude": [
|
|
17
|
+
"node_modules"
|
|
18
|
+
]
|
|
19
|
+
}
|