reddit-mcp-server 1.0.4 → 1.0.6

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,4 +1,3 @@
1
- import axios, { AxiosInstance } from "axios"
2
1
  import { RedditClientConfig, RedditUser, RedditPost, RedditComment, RedditSubreddit } from "../types"
3
2
 
4
3
  export class RedditClient {
@@ -9,7 +8,7 @@ export class RedditClient {
9
8
  private password?: string
10
9
  private accessToken?: string
11
10
  private tokenExpiry: number = 0
12
- private api: AxiosInstance
11
+ private baseUrl: string = "https://oauth.reddit.com"
13
12
  private authenticated: boolean = false
14
13
 
15
14
  constructor(config: RedditClientConfig) {
@@ -18,27 +17,40 @@ export class RedditClient {
18
17
  this.userAgent = config.userAgent
19
18
  this.username = config.username
20
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
+ }
21
34
 
22
- this.api = axios.create({
23
- baseURL: "https://oauth.reddit.com",
24
- headers: {
25
- "User-Agent": this.userAgent,
26
- },
35
+ const response = await fetch(url, {
36
+ ...options,
37
+ headers,
27
38
  })
28
39
 
29
- // Add response interceptor to handle token refresh
30
- this.api.interceptors.response.use(
31
- (response) => response,
32
- async (error) => {
33
- if (error.response?.status === 401 && this.authenticated) {
34
- await this.authenticate()
35
- const originalRequest = error.config
36
- originalRequest.headers["Authorization"] = `Bearer ${this.accessToken}`
37
- return this.api(originalRequest)
38
- }
39
- return Promise.reject(error)
40
- },
41
- )
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
42
54
  }
43
55
 
44
56
  async authenticate(): Promise<void> {
@@ -61,21 +73,25 @@ export class RedditClient {
61
73
  authData.append("grant_type", "client_credentials")
62
74
  }
63
75
 
64
- const response = await axios.post(authUrl, authData, {
65
- auth: {
66
- username: this.clientId,
67
- password: this.clientSecret,
68
- },
76
+ const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")
77
+ const response = await fetch(authUrl, {
78
+ method: "POST",
69
79
  headers: {
70
80
  "User-Agent": this.userAgent,
71
81
  "Content-Type": "application/x-www-form-urlencoded",
82
+ Authorization: `Basic ${credentials}`,
72
83
  },
84
+ body: authData.toString(),
73
85
  })
74
86
 
75
- this.accessToken = response.data.access_token
76
- this.tokenExpiry = now + response.data.expires_in * 1000
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
77
94
  this.authenticated = true
78
- this.api.defaults.headers.common["Authorization"] = `Bearer ${this.accessToken}`
79
95
 
80
96
  // Successfully authenticated with Reddit API
81
97
  } catch {
@@ -99,8 +115,13 @@ export class RedditClient {
99
115
  async getUser(username: string): Promise<RedditUser> {
100
116
  await this.authenticate()
101
117
  try {
102
- const response = await this.api.get(`/user/${username}/about.json`)
103
- const data = response.data.data
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
104
125
 
105
126
  return {
106
127
  name: data.name,
@@ -123,8 +144,13 @@ export class RedditClient {
123
144
  async getSubredditInfo(subredditName: string): Promise<RedditSubreddit> {
124
145
  await this.authenticate()
125
146
  try {
126
- const response = await this.api.get(`/r/${subredditName}/about.json`)
127
- const data = response.data.data
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
128
154
 
129
155
  return {
130
156
  displayName: data.display_name,
@@ -148,14 +174,19 @@ export class RedditClient {
148
174
  await this.authenticate()
149
175
  try {
150
176
  const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json"
151
- const response = await this.api.get(endpoint, {
152
- params: {
153
- t: timeFilter,
154
- limit,
155
- },
177
+ const params = new URLSearchParams({
178
+ t: timeFilter,
179
+ limit: limit.toString(),
156
180
  })
157
181
 
158
- return response.data.data.children.map((child: any) => {
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) => {
159
190
  const post = child.data
160
191
  return {
161
192
  id: post.id,
@@ -186,19 +217,24 @@ export class RedditClient {
186
217
  await this.authenticate()
187
218
  try {
188
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
+ }
189
225
 
190
- const response = await this.api.get(endpoint)
226
+ const json = (await response.json()) as any
191
227
 
192
228
  let post
193
229
  if (subreddit) {
194
230
  // When using the comments endpoint
195
- post = response.data[0].data.children[0].data
231
+ post = json[0].data.children[0].data
196
232
  } else {
197
233
  // When using the info endpoint
198
- if (!response.data.data.children.length) {
234
+ if (!json.data.children.length) {
199
235
  throw new Error(`Post with ID ${postId} not found`)
200
236
  }
201
- post = response.data.data.children[0].data
237
+ post = json.data.children[0].data
202
238
  }
203
239
 
204
240
  return {
@@ -228,11 +264,15 @@ export class RedditClient {
228
264
  async getTrendingSubreddits(limit: number = 5): Promise<string[]> {
229
265
  await this.authenticate()
230
266
  try {
231
- const response = await this.api.get("/subreddits/popular.json", {
232
- params: { limit },
233
- })
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
+ }
234
273
 
235
- return response.data.data.children.map((child: any) => child.data.display_name)
274
+ const json = (await response.json()) as { data: { children: any[] } }
275
+ return json.data.children.map((child: any) => child.data.display_name)
236
276
  } catch {
237
277
  // Failed to get trending subreddits
238
278
  throw new Error("Failed to get trending subreddits")
@@ -254,15 +294,22 @@ export class RedditClient {
254
294
  params.append("title", title)
255
295
  params.append(isSelf ? "text" : "url", content)
256
296
 
257
- const response = await this.api.post("/api/submit", params, {
297
+ const response = await this.makeRequest("/api/submit", {
298
+ method: "POST",
258
299
  headers: {
259
300
  "Content-Type": "application/x-www-form-urlencoded",
260
301
  },
302
+ body: params.toString(),
261
303
  })
262
304
 
263
- if (response.data.success) {
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) {
264
311
  // Get the newly created post
265
- const postId = response.data.data.id
312
+ const postId = json.data.id
266
313
  return await this.getPost(postId)
267
314
  } else {
268
315
  throw new Error("Failed to create post")
@@ -276,8 +323,13 @@ export class RedditClient {
276
323
  async checkPostExists(postId: string): Promise<boolean> {
277
324
  await this.authenticate()
278
325
  try {
279
- const response = await this.api.get(`/api/info.json?id=t3_${postId}`)
280
- return response.data.data.children.length > 0
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
281
333
  } catch {
282
334
  return false
283
335
  }
@@ -299,14 +351,20 @@ export class RedditClient {
299
351
  params.append("thing_id", `t3_${postId}`)
300
352
  params.append("text", content)
301
353
 
302
- const response = await this.api.post("/api/comment", params, {
354
+ const response = await this.makeRequest("/api/comment", {
355
+ method: "POST",
303
356
  headers: {
304
357
  "Content-Type": "application/x-www-form-urlencoded",
305
358
  },
359
+ body: params.toString(),
306
360
  })
307
361
 
362
+ if (!response.ok) {
363
+ throw new Error(`HTTP ${response.status}`)
364
+ }
365
+
308
366
  // Extract comment data from response
309
- const commentData = response.data
367
+ const commentData = (await response.json()) as any
310
368
  return {
311
369
  id: commentData.id,
312
370
  author: this.username,
@@ -325,6 +383,246 @@ export class RedditClient {
325
383
  throw new Error(`Failed to reply to post ${postId}`)
326
384
  }
327
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
+ }
328
626
  }
329
627
 
330
628
  // Create and export singleton instance