reddit-mcp-server 1.0.6 → 1.0.7

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.
@@ -1,638 +0,0 @@
1
- import { RedditClientConfig, RedditUser, RedditPost, RedditComment, RedditSubreddit } from "../types"
2
-
3
- export class RedditClient {
4
- private clientId: string
5
- private clientSecret: string
6
- private userAgent: string
7
- private username?: string
8
- private password?: string
9
- private accessToken?: string
10
- private tokenExpiry: number = 0
11
- private baseUrl: string = "https://oauth.reddit.com"
12
- private authenticated: boolean = false
13
-
14
- constructor(config: RedditClientConfig) {
15
- this.clientId = config.clientId
16
- this.clientSecret = config.clientSecret
17
- this.userAgent = config.userAgent
18
- this.username = config.username
19
- this.password = config.password
20
- }
21
-
22
- private async makeRequest(path: string, options: RequestInit = {}): Promise<Response> {
23
- // Check if we need to refresh token
24
- if (Date.now() >= this.tokenExpiry || !this.authenticated) {
25
- await this.authenticate()
26
- }
27
-
28
- const url = `${this.baseUrl}${path}`
29
- const headers = {
30
- "User-Agent": this.userAgent,
31
- Authorization: `Bearer ${this.accessToken}`,
32
- ...options.headers,
33
- }
34
-
35
- const response = await fetch(url, {
36
- ...options,
37
- headers,
38
- })
39
-
40
- // If unauthorized, try to refresh token and retry once
41
- if (response.status === 401 && this.authenticated) {
42
- await this.authenticate()
43
- const retryHeaders = {
44
- ...headers,
45
- Authorization: `Bearer ${this.accessToken}`,
46
- }
47
- return fetch(url, {
48
- ...options,
49
- headers: retryHeaders,
50
- })
51
- }
52
-
53
- return response
54
- }
55
-
56
- async authenticate(): Promise<void> {
57
- try {
58
- const now = Date.now()
59
- if (this.accessToken && now < this.tokenExpiry) {
60
- return
61
- }
62
-
63
- const authUrl = "https://www.reddit.com/api/v1/access_token"
64
- const authData = new URLSearchParams()
65
-
66
- if (this.username && this.password) {
67
- // Authenticating with user credentials
68
- authData.append("grant_type", "password")
69
- authData.append("username", this.username)
70
- authData.append("password", this.password)
71
- } else {
72
- // Authenticating with client credentials (read-only)
73
- authData.append("grant_type", "client_credentials")
74
- }
75
-
76
- const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")
77
- const response = await fetch(authUrl, {
78
- method: "POST",
79
- headers: {
80
- "User-Agent": this.userAgent,
81
- "Content-Type": "application/x-www-form-urlencoded",
82
- Authorization: `Basic ${credentials}`,
83
- },
84
- body: authData.toString(),
85
- })
86
-
87
- if (!response.ok) {
88
- throw new Error(`Authentication failed: ${response.status}`)
89
- }
90
-
91
- const data = (await response.json()) as { access_token: string; expires_in: number }
92
- this.accessToken = data.access_token
93
- this.tokenExpiry = now + data.expires_in * 1000
94
- this.authenticated = true
95
-
96
- // Successfully authenticated with Reddit API
97
- } catch {
98
- // Authentication error occurred
99
- throw new Error("Failed to authenticate with Reddit API")
100
- }
101
- }
102
-
103
- async checkAuthentication(): Promise<boolean> {
104
- if (!this.authenticated) {
105
- try {
106
- await this.authenticate()
107
- return true
108
- } catch {
109
- return false
110
- }
111
- }
112
- return true
113
- }
114
-
115
- async getUser(username: string): Promise<RedditUser> {
116
- await this.authenticate()
117
- try {
118
- const response = await this.makeRequest(`/user/${username}/about.json`)
119
- if (!response.ok) {
120
- throw new Error(`HTTP ${response.status}`)
121
- }
122
-
123
- const json = (await response.json()) as { data: any }
124
- const data = json.data
125
-
126
- return {
127
- name: data.name,
128
- id: data.id,
129
- commentKarma: data.comment_karma,
130
- linkKarma: data.link_karma,
131
- totalKarma: data.total_karma || data.comment_karma + data.link_karma,
132
- isMod: data.is_mod,
133
- isGold: data.is_gold,
134
- isEmployee: data.is_employee,
135
- createdUtc: data.created_utc,
136
- profileUrl: `https://reddit.com/user/${data.name}`,
137
- }
138
- } catch {
139
- // Failed to get user info
140
- throw new Error(`Failed to get user info for ${username}`)
141
- }
142
- }
143
-
144
- async getSubredditInfo(subredditName: string): Promise<RedditSubreddit> {
145
- await this.authenticate()
146
- try {
147
- const response = await this.makeRequest(`/r/${subredditName}/about.json`)
148
- if (!response.ok) {
149
- throw new Error(`HTTP ${response.status}`)
150
- }
151
-
152
- const json = (await response.json()) as { data: any }
153
- const data = json.data
154
-
155
- return {
156
- displayName: data.display_name,
157
- title: data.title,
158
- description: data.description || "",
159
- publicDescription: data.public_description || "",
160
- subscribers: data.subscribers,
161
- activeUserCount: data.active_user_count,
162
- createdUtc: data.created_utc,
163
- over18: data.over18,
164
- subredditType: data.subreddit_type,
165
- url: data.url,
166
- }
167
- } catch {
168
- // Failed to get subreddit info
169
- throw new Error(`Failed to get subreddit info for ${subredditName}`)
170
- }
171
- }
172
-
173
- async getTopPosts(subreddit: string, timeFilter: string = "week", limit: number = 10): Promise<RedditPost[]> {
174
- await this.authenticate()
175
- try {
176
- const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json"
177
- const params = new URLSearchParams({
178
- t: timeFilter,
179
- limit: limit.toString(),
180
- })
181
-
182
- const response = await this.makeRequest(`${endpoint}?${params}`)
183
- if (!response.ok) {
184
- throw new Error(`HTTP ${response.status}`)
185
- }
186
-
187
- const json = (await response.json()) as { data: { children: any[] } }
188
-
189
- return json.data.children.map((child: any) => {
190
- const post = child.data
191
- return {
192
- id: post.id,
193
- title: post.title,
194
- author: post.author,
195
- subreddit: post.subreddit,
196
- selftext: post.selftext,
197
- url: post.url,
198
- score: post.score,
199
- upvoteRatio: post.upvote_ratio,
200
- numComments: post.num_comments,
201
- createdUtc: post.created_utc,
202
- over18: post.over_18,
203
- spoiler: post.spoiler,
204
- edited: !!post.edited,
205
- isSelf: post.is_self,
206
- linkFlairText: post.link_flair_text,
207
- permalink: post.permalink,
208
- }
209
- })
210
- } catch {
211
- // Failed to get top posts
212
- throw new Error(`Failed to get top posts for ${subreddit || "home"}`)
213
- }
214
- }
215
-
216
- async getPost(postId: string, subreddit?: string): Promise<RedditPost> {
217
- await this.authenticate()
218
- try {
219
- const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`
220
- const response = await this.makeRequest(endpoint)
221
-
222
- if (!response.ok) {
223
- throw new Error(`HTTP ${response.status}`)
224
- }
225
-
226
- const json = (await response.json()) as any
227
-
228
- let post
229
- if (subreddit) {
230
- // When using the comments endpoint
231
- post = json[0].data.children[0].data
232
- } else {
233
- // When using the info endpoint
234
- if (!json.data.children.length) {
235
- throw new Error(`Post with ID ${postId} not found`)
236
- }
237
- post = json.data.children[0].data
238
- }
239
-
240
- return {
241
- id: post.id,
242
- title: post.title,
243
- author: post.author,
244
- subreddit: post.subreddit,
245
- selftext: post.selftext,
246
- url: post.url,
247
- score: post.score,
248
- upvoteRatio: post.upvote_ratio,
249
- numComments: post.num_comments,
250
- createdUtc: post.created_utc,
251
- over18: post.over_18,
252
- spoiler: post.spoiler,
253
- edited: !!post.edited,
254
- isSelf: post.is_self,
255
- linkFlairText: post.link_flair_text,
256
- permalink: post.permalink,
257
- }
258
- } catch {
259
- // Failed to get post
260
- throw new Error(`Failed to get post with ID ${postId}`)
261
- }
262
- }
263
-
264
- async getTrendingSubreddits(limit: number = 5): Promise<string[]> {
265
- await this.authenticate()
266
- try {
267
- const params = new URLSearchParams({ limit: limit.toString() })
268
- const response = await this.makeRequest(`/subreddits/popular.json?${params}`)
269
-
270
- if (!response.ok) {
271
- throw new Error(`HTTP ${response.status}`)
272
- }
273
-
274
- const json = (await response.json()) as { data: { children: any[] } }
275
- return json.data.children.map((child: any) => child.data.display_name)
276
- } catch {
277
- // Failed to get trending subreddits
278
- throw new Error("Failed to get trending subreddits")
279
- }
280
- }
281
-
282
- async createPost(subreddit: string, title: string, content: string, isSelf: boolean = true): Promise<RedditPost> {
283
- await this.authenticate()
284
-
285
- if (!this.username || !this.password) {
286
- throw new Error("User authentication required for posting")
287
- }
288
-
289
- try {
290
- const kind = isSelf ? "self" : "link"
291
- const params = new URLSearchParams()
292
- params.append("sr", subreddit)
293
- params.append("kind", kind)
294
- params.append("title", title)
295
- params.append(isSelf ? "text" : "url", content)
296
-
297
- const response = await this.makeRequest("/api/submit", {
298
- method: "POST",
299
- headers: {
300
- "Content-Type": "application/x-www-form-urlencoded",
301
- },
302
- body: params.toString(),
303
- })
304
-
305
- if (!response.ok) {
306
- throw new Error(`HTTP ${response.status}`)
307
- }
308
-
309
- const json = (await response.json()) as { success: boolean; data: { id: string } }
310
- if (json.success) {
311
- // Get the newly created post
312
- const postId = json.data.id
313
- return await this.getPost(postId)
314
- } else {
315
- throw new Error("Failed to create post")
316
- }
317
- } catch {
318
- // Failed to create post
319
- throw new Error(`Failed to create post in ${subreddit}`)
320
- }
321
- }
322
-
323
- async checkPostExists(postId: string): Promise<boolean> {
324
- await this.authenticate()
325
- try {
326
- const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`)
327
- if (!response.ok) {
328
- return false
329
- }
330
-
331
- const json = (await response.json()) as { data: { children: any[] } }
332
- return json.data.children.length > 0
333
- } catch {
334
- return false
335
- }
336
- }
337
-
338
- async replyToPost(postId: string, content: string): Promise<RedditComment> {
339
- await this.authenticate()
340
-
341
- if (!this.username || !this.password) {
342
- throw new Error("User authentication required for posting replies")
343
- }
344
-
345
- try {
346
- if (!(await this.checkPostExists(postId))) {
347
- throw new Error(`Post with ID ${postId} does not exist or is not accessible`)
348
- }
349
-
350
- const params = new URLSearchParams()
351
- params.append("thing_id", `t3_${postId}`)
352
- params.append("text", content)
353
-
354
- const response = await this.makeRequest("/api/comment", {
355
- method: "POST",
356
- headers: {
357
- "Content-Type": "application/x-www-form-urlencoded",
358
- },
359
- body: params.toString(),
360
- })
361
-
362
- if (!response.ok) {
363
- throw new Error(`HTTP ${response.status}`)
364
- }
365
-
366
- // Extract comment data from response
367
- const commentData = (await response.json()) as any
368
- return {
369
- id: commentData.id,
370
- author: this.username,
371
- body: content,
372
- score: 1,
373
- controversiality: 0,
374
- subreddit: commentData.subreddit,
375
- submissionTitle: commentData.link_title,
376
- createdUtc: Date.now() / 1000,
377
- edited: false,
378
- isSubmitter: false,
379
- permalink: commentData.permalink,
380
- }
381
- } catch {
382
- // Failed to reply to post
383
- throw new Error(`Failed to reply to post ${postId}`)
384
- }
385
- }
386
-
387
- async searchReddit(
388
- query: string,
389
- options: {
390
- subreddit?: string
391
- sort?: string
392
- timeFilter?: string
393
- limit?: number
394
- type?: string
395
- } = {},
396
- ): Promise<RedditPost[]> {
397
- await this.authenticate()
398
- try {
399
- const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options
400
- const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json"
401
-
402
- const params = new URLSearchParams({
403
- q: query,
404
- sort,
405
- t: timeFilter,
406
- limit: limit.toString(),
407
- type,
408
- ...(subreddit && { restrict_sr: "true" }),
409
- })
410
-
411
- const response = await this.makeRequest(`${endpoint}?${params}`)
412
- if (!response.ok) {
413
- throw new Error(`HTTP ${response.status}`)
414
- }
415
-
416
- const json = (await response.json()) as { data: { children: any[] } }
417
-
418
- return json.data.children
419
- .filter((child: any) => child.kind === "t3") // Only posts
420
- .map((child: any) => {
421
- const post = child.data
422
- return {
423
- id: post.id,
424
- title: post.title,
425
- author: post.author,
426
- subreddit: post.subreddit,
427
- selftext: post.selftext || "",
428
- url: post.url,
429
- score: post.score,
430
- upvoteRatio: post.upvote_ratio,
431
- numComments: post.num_comments,
432
- createdUtc: post.created_utc,
433
- over18: post.over_18,
434
- spoiler: post.spoiler,
435
- edited: !!post.edited,
436
- isSelf: post.is_self,
437
- linkFlairText: post.link_flair_text,
438
- permalink: post.permalink,
439
- }
440
- })
441
- } catch {
442
- throw new Error(`Failed to search Reddit for: ${query}`)
443
- }
444
- }
445
-
446
- async getPostComments(
447
- postId: string,
448
- subreddit: string,
449
- options: {
450
- sort?: string
451
- limit?: number
452
- } = {},
453
- ): Promise<{ post: RedditPost; comments: RedditComment[] }> {
454
- await this.authenticate()
455
- try {
456
- const { sort = "best", limit = 100 } = options
457
- const params = new URLSearchParams({
458
- sort,
459
- limit: limit.toString(),
460
- })
461
-
462
- const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`)
463
- if (!response.ok) {
464
- throw new Error(`HTTP ${response.status}`)
465
- }
466
-
467
- const json = (await response.json()) as any[]
468
-
469
- // First element is the post, second is the comments
470
- const postData = json[0].data.children[0].data
471
- const post: RedditPost = {
472
- id: postData.id,
473
- title: postData.title,
474
- author: postData.author,
475
- subreddit: postData.subreddit,
476
- selftext: postData.selftext || "",
477
- url: postData.url,
478
- score: postData.score,
479
- upvoteRatio: postData.upvote_ratio,
480
- numComments: postData.num_comments,
481
- createdUtc: postData.created_utc,
482
- over18: postData.over_18,
483
- spoiler: postData.spoiler,
484
- edited: !!postData.edited,
485
- isSelf: postData.is_self,
486
- linkFlairText: postData.link_flair_text,
487
- permalink: postData.permalink,
488
- }
489
-
490
- const comments: RedditComment[] = []
491
- const parseComments = (commentData: any[], depth: number = 0) => {
492
- for (const item of commentData) {
493
- if (item.kind === "t1" && item.data.body) {
494
- comments.push({
495
- id: item.data.id,
496
- author: item.data.author,
497
- body: item.data.body,
498
- score: item.data.score,
499
- controversiality: item.data.controversiality,
500
- subreddit: item.data.subreddit,
501
- submissionTitle: post.title,
502
- createdUtc: item.data.created_utc,
503
- edited: !!item.data.edited,
504
- isSubmitter: item.data.is_submitter,
505
- permalink: item.data.permalink,
506
- depth,
507
- parentId: item.data.parent_id,
508
- })
509
-
510
- // Parse replies recursively
511
- if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
512
- parseComments(item.data.replies.data.children, depth + 1)
513
- }
514
- }
515
- }
516
- }
517
-
518
- if (json[1] && json[1].data && json[1].data.children) {
519
- parseComments(json[1].data.children)
520
- }
521
-
522
- return { post, comments }
523
- } catch {
524
- throw new Error(`Failed to get comments for post ${postId}`)
525
- }
526
- }
527
-
528
- async getUserPosts(
529
- username: string,
530
- options: {
531
- sort?: string
532
- timeFilter?: string
533
- limit?: number
534
- } = {},
535
- ): Promise<RedditPost[]> {
536
- await this.authenticate()
537
- try {
538
- const { sort = "new", timeFilter = "all", limit = 25 } = options
539
- const params = new URLSearchParams({
540
- sort,
541
- t: timeFilter,
542
- limit: limit.toString(),
543
- })
544
-
545
- const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`)
546
- if (!response.ok) {
547
- throw new Error(`HTTP ${response.status}`)
548
- }
549
-
550
- const json = (await response.json()) as { data: { children: any[] } }
551
-
552
- return json.data.children
553
- .filter((child: any) => child.kind === "t3")
554
- .map((child: any) => {
555
- const post = child.data
556
- return {
557
- id: post.id,
558
- title: post.title,
559
- author: post.author,
560
- subreddit: post.subreddit,
561
- selftext: post.selftext || "",
562
- url: post.url,
563
- score: post.score,
564
- upvoteRatio: post.upvote_ratio,
565
- numComments: post.num_comments,
566
- createdUtc: post.created_utc,
567
- over18: post.over_18,
568
- spoiler: post.spoiler,
569
- edited: !!post.edited,
570
- isSelf: post.is_self,
571
- linkFlairText: post.link_flair_text,
572
- permalink: post.permalink,
573
- }
574
- })
575
- } catch {
576
- throw new Error(`Failed to get posts for user ${username}`)
577
- }
578
- }
579
-
580
- async getUserComments(
581
- username: string,
582
- options: {
583
- sort?: string
584
- timeFilter?: string
585
- limit?: number
586
- } = {},
587
- ): Promise<RedditComment[]> {
588
- await this.authenticate()
589
- try {
590
- const { sort = "new", timeFilter = "all", limit = 25 } = options
591
- const params = new URLSearchParams({
592
- sort,
593
- t: timeFilter,
594
- limit: limit.toString(),
595
- })
596
-
597
- const response = await this.makeRequest(`/user/${username}/comments.json?${params}`)
598
- if (!response.ok) {
599
- throw new Error(`HTTP ${response.status}`)
600
- }
601
-
602
- const json = (await response.json()) as { data: { children: any[] } }
603
-
604
- return json.data.children
605
- .filter((child: any) => child.kind === "t1")
606
- .map((child: any) => {
607
- const comment = child.data
608
- return {
609
- id: comment.id,
610
- author: comment.author,
611
- body: comment.body,
612
- score: comment.score,
613
- controversiality: comment.controversiality,
614
- subreddit: comment.subreddit,
615
- submissionTitle: comment.link_title || "",
616
- createdUtc: comment.created_utc,
617
- edited: !!comment.edited,
618
- isSubmitter: comment.is_submitter,
619
- permalink: comment.permalink,
620
- }
621
- })
622
- } catch {
623
- throw new Error(`Failed to get comments for user ${username}`)
624
- }
625
- }
626
- }
627
-
628
- // Create and export singleton instance
629
- let redditClient: RedditClient | null = null
630
-
631
- export function initializeRedditClient(config: RedditClientConfig): RedditClient {
632
- redditClient = new RedditClient(config)
633
- return redditClient
634
- }
635
-
636
- export function getRedditClient(): RedditClient | null {
637
- return redditClient
638
- }