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.
package/src/index.ts CHANGED
@@ -52,10 +52,7 @@ class RedditServer {
52
52
 
53
53
  if (!clientId || !clientSecret) {
54
54
  // Can't use server logging here as server isn't initialized yet
55
- // Using console.error is OK for startup failures
56
- console.error(
57
- "[Error] Missing required Reddit API credentials. Please set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables.",
58
- )
55
+ // Exit silently with error code
59
56
  process.exit(1)
60
57
  }
61
58
 
@@ -69,9 +66,9 @@ class RedditServer {
69
66
  })
70
67
 
71
68
  // Client initialized successfully
72
- } catch (error) {
69
+ } catch {
73
70
  // Can't use server logging here as server isn't connected yet
74
- console.error("[Error] Failed to initialize Reddit client:", error)
71
+ // Exit silently with error code
75
72
  process.exit(1)
76
73
  }
77
74
  }
@@ -217,6 +214,146 @@ class RedditServer {
217
214
  required: ["post_id", "content"],
218
215
  },
219
216
  },
217
+ {
218
+ name: "search_reddit",
219
+ description: "Search for posts on Reddit",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ query: {
224
+ type: "string",
225
+ description: "The search query",
226
+ },
227
+ subreddit: {
228
+ type: "string",
229
+ description: "Search within a specific subreddit (optional)",
230
+ },
231
+ sort: {
232
+ type: "string",
233
+ description: "Sort order: relevance, hot, top, new, comments",
234
+ enum: ["relevance", "hot", "top", "new", "comments"],
235
+ default: "relevance",
236
+ },
237
+ time_filter: {
238
+ type: "string",
239
+ description: "Time filter: hour, day, week, month, year, all",
240
+ enum: ["hour", "day", "week", "month", "year", "all"],
241
+ default: "all",
242
+ },
243
+ limit: {
244
+ type: "number",
245
+ description: "Maximum number of results to return",
246
+ minimum: 1,
247
+ maximum: 100,
248
+ default: 10,
249
+ },
250
+ type: {
251
+ type: "string",
252
+ description: "Type of content: link (posts), sr (subreddits), user (users)",
253
+ enum: ["link", "sr", "user"],
254
+ default: "link",
255
+ },
256
+ },
257
+ required: ["query"],
258
+ },
259
+ },
260
+ {
261
+ name: "get_post_comments",
262
+ description: "Get comments for a specific Reddit post",
263
+ inputSchema: {
264
+ type: "object",
265
+ properties: {
266
+ post_id: {
267
+ type: "string",
268
+ description: "The ID of the post",
269
+ },
270
+ subreddit: {
271
+ type: "string",
272
+ description: "The subreddit where the post is located",
273
+ },
274
+ sort: {
275
+ type: "string",
276
+ description: "Comment sort order: best, top, new, controversial, old, qa",
277
+ enum: ["best", "top", "new", "controversial", "old", "qa"],
278
+ default: "best",
279
+ },
280
+ limit: {
281
+ type: "number",
282
+ description: "Maximum number of comments to load",
283
+ minimum: 1,
284
+ maximum: 500,
285
+ default: 100,
286
+ },
287
+ },
288
+ required: ["post_id", "subreddit"],
289
+ },
290
+ },
291
+ {
292
+ name: "get_user_posts",
293
+ description: "Get posts submitted by a specific user",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ username: {
298
+ type: "string",
299
+ description: "The username to get posts for",
300
+ },
301
+ sort: {
302
+ type: "string",
303
+ description: "Sort order: new, hot, top, controversial",
304
+ enum: ["new", "hot", "top", "controversial"],
305
+ default: "new",
306
+ },
307
+ time_filter: {
308
+ type: "string",
309
+ description: "Time filter for top/controversial: hour, day, week, month, year, all",
310
+ enum: ["hour", "day", "week", "month", "year", "all"],
311
+ default: "all",
312
+ },
313
+ limit: {
314
+ type: "number",
315
+ description: "Maximum number of posts to return",
316
+ minimum: 1,
317
+ maximum: 100,
318
+ default: 10,
319
+ },
320
+ },
321
+ required: ["username"],
322
+ },
323
+ },
324
+ {
325
+ name: "get_user_comments",
326
+ description: "Get comments made by a specific user",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ username: {
331
+ type: "string",
332
+ description: "The username to get comments for",
333
+ },
334
+ sort: {
335
+ type: "string",
336
+ description: "Sort order: new, hot, top, controversial",
337
+ enum: ["new", "hot", "top", "controversial"],
338
+ default: "new",
339
+ },
340
+ time_filter: {
341
+ type: "string",
342
+ description: "Time filter for top/controversial: hour, day, week, month, year, all",
343
+ enum: ["hour", "day", "week", "month", "year", "all"],
344
+ default: "all",
345
+ },
346
+ limit: {
347
+ type: "number",
348
+ description: "Maximum number of comments to return",
349
+ minimum: 1,
350
+ maximum: 100,
351
+ default: 10,
352
+ },
353
+ },
354
+ required: ["username"],
355
+ },
356
+ },
220
357
  ],
221
358
  }))
222
359
 
@@ -283,6 +420,48 @@ class RedditServer {
283
420
  },
284
421
  )
285
422
 
423
+ case "search_reddit":
424
+ return await tools.searchReddit(
425
+ toolParams as {
426
+ query: string
427
+ subreddit?: string
428
+ sort?: string
429
+ time_filter?: string
430
+ limit?: number
431
+ type?: string
432
+ },
433
+ )
434
+
435
+ case "get_post_comments":
436
+ return await tools.getPostComments(
437
+ toolParams as {
438
+ post_id: string
439
+ subreddit: string
440
+ sort?: string
441
+ limit?: number
442
+ },
443
+ )
444
+
445
+ case "get_user_posts":
446
+ return await tools.getUserPosts(
447
+ toolParams as {
448
+ username: string
449
+ sort?: string
450
+ time_filter?: string
451
+ limit?: number
452
+ },
453
+ )
454
+
455
+ case "get_user_comments":
456
+ return await tools.getUserComments(
457
+ toolParams as {
458
+ username: string
459
+ sort?: string
460
+ time_filter?: string
461
+ limit?: number
462
+ },
463
+ )
464
+
286
465
  default:
287
466
  throw new McpError(ErrorCode.MethodNotFound, `Tool with name ${toolName} not found`)
288
467
  }
@@ -330,7 +509,10 @@ class RedditServer {
330
509
  // Only run if this is the main module
331
510
  if (require.main === module) {
332
511
  const server = new RedditServer()
333
- server.run().catch(console.error)
512
+ server.run().catch(() => {
513
+ // Exit silently on error
514
+ process.exit(1)
515
+ })
334
516
  }
335
517
 
336
518
  export { RedditServer }
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { getPostComments } from "../comment-tools"
3
+ import { getRedditClient } from "../../client/reddit-client"
4
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"
5
+
6
+ vi.mock("../../client/reddit-client")
7
+
8
+ describe("getPostComments", () => {
9
+ const mockRedditClient = {
10
+ getPostComments: vi.fn(),
11
+ }
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks()
15
+ vi.mocked(getRedditClient).mockReturnValue(mockRedditClient as any)
16
+ })
17
+
18
+ it("should fetch post comments with default parameters", async () => {
19
+ const mockData = {
20
+ post: {
21
+ id: "test123",
22
+ title: "Test Post Title",
23
+ author: "testauthor",
24
+ subreddit: "testsubreddit",
25
+ score: 500,
26
+ upvoteRatio: 0.9,
27
+ numComments: 25,
28
+ createdUtc: 1700000000,
29
+ selftext: "This is the post content",
30
+ permalink: "/r/testsubreddit/comments/test123",
31
+ over18: false,
32
+ spoiler: false,
33
+ },
34
+ comments: [
35
+ {
36
+ id: "comment1",
37
+ author: "commenter1",
38
+ body: "This is a comment",
39
+ score: 10,
40
+ createdUtc: 1700000100,
41
+ edited: false,
42
+ isSubmitter: false,
43
+ depth: 0,
44
+ },
45
+ {
46
+ id: "comment2",
47
+ author: "testauthor",
48
+ body: "This is a reply from OP",
49
+ score: 5,
50
+ createdUtc: 1700000200,
51
+ edited: true,
52
+ isSubmitter: true,
53
+ depth: 1,
54
+ },
55
+ ],
56
+ }
57
+
58
+ mockRedditClient.getPostComments.mockResolvedValue(mockData)
59
+
60
+ const result = await getPostComments({
61
+ post_id: "test123",
62
+ subreddit: "testsubreddit",
63
+ })
64
+
65
+ expect(mockRedditClient.getPostComments).toHaveBeenCalledWith("test123", "testsubreddit", {
66
+ sort: "best",
67
+ limit: 100,
68
+ })
69
+
70
+ expect(result.content).toHaveLength(1)
71
+ expect(result.content[0].type).toBe("text")
72
+ expect(result.content[0].text).toContain("Comments for: Test Post Title")
73
+ expect(result.content[0].text).toContain("**u/commenter1**")
74
+ expect(result.content[0].text).toContain("**u/testauthor** **[OP]**")
75
+ expect(result.content[0].text).toContain("└─")
76
+ })
77
+
78
+ it("should fetch comments with custom parameters", async () => {
79
+ mockRedditClient.getPostComments.mockResolvedValue({ post: {}, comments: [] })
80
+
81
+ await getPostComments({
82
+ post_id: "test456",
83
+ subreddit: "programming",
84
+ sort: "controversial",
85
+ limit: 50,
86
+ })
87
+
88
+ expect(mockRedditClient.getPostComments).toHaveBeenCalledWith("test456", "programming", {
89
+ sort: "controversial",
90
+ limit: 50,
91
+ })
92
+ })
93
+
94
+ it("should throw error if Reddit client is not initialized", async () => {
95
+ vi.mocked(getRedditClient).mockReturnValue(null)
96
+
97
+ await expect(getPostComments({ post_id: "test", subreddit: "test" })).rejects.toThrow(
98
+ new McpError(ErrorCode.InternalError, "Reddit client not initialized"),
99
+ )
100
+ })
101
+
102
+ it("should throw error if required parameters are missing", async () => {
103
+ await expect(getPostComments({ post_id: "", subreddit: "test" })).rejects.toThrow(
104
+ new McpError(ErrorCode.InvalidParams, "post_id and subreddit are required"),
105
+ )
106
+
107
+ await expect(getPostComments({ post_id: "test", subreddit: "" })).rejects.toThrow(
108
+ new McpError(ErrorCode.InvalidParams, "post_id and subreddit are required"),
109
+ )
110
+ })
111
+
112
+ it("should handle API errors", async () => {
113
+ mockRedditClient.getPostComments.mockRejectedValue(new Error("API Error"))
114
+
115
+ await expect(getPostComments({ post_id: "test", subreddit: "test" })).rejects.toThrow(
116
+ new McpError(ErrorCode.InternalError, "Failed to fetch comments: Error: API Error"),
117
+ )
118
+ })
119
+ })
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { searchReddit } from "../search-tools"
3
+ import { getRedditClient } from "../../client/reddit-client"
4
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"
5
+
6
+ vi.mock("../../client/reddit-client")
7
+
8
+ describe("searchReddit", () => {
9
+ const mockRedditClient = {
10
+ searchReddit: vi.fn(),
11
+ }
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks()
15
+ vi.mocked(getRedditClient).mockReturnValue(mockRedditClient as any)
16
+ })
17
+
18
+ it("should search Reddit with default parameters", async () => {
19
+ const mockPosts = [
20
+ {
21
+ id: "test1",
22
+ title: "Test Post 1",
23
+ author: "testuser1",
24
+ subreddit: "testsubreddit",
25
+ score: 100,
26
+ upvoteRatio: 0.95,
27
+ numComments: 10,
28
+ createdUtc: 1700000000,
29
+ selftext: "This is a test post",
30
+ permalink: "/r/testsubreddit/comments/test1",
31
+ over18: false,
32
+ spoiler: false,
33
+ },
34
+ ]
35
+
36
+ mockRedditClient.searchReddit.mockResolvedValue(mockPosts)
37
+
38
+ const result = await searchReddit({ query: "test query" })
39
+
40
+ expect(mockRedditClient.searchReddit).toHaveBeenCalledWith("test query", {
41
+ subreddit: undefined,
42
+ sort: "relevance",
43
+ timeFilter: "all",
44
+ limit: 10,
45
+ type: "link",
46
+ })
47
+
48
+ expect(result.content).toHaveLength(1)
49
+ expect(result.content[0].type).toBe("text")
50
+ expect(result.content[0].text).toContain('Reddit Search Results for: "test query"')
51
+ expect(result.content[0].text).toContain("Test Post 1")
52
+ })
53
+
54
+ it("should search within a specific subreddit", async () => {
55
+ mockRedditClient.searchReddit.mockResolvedValue([])
56
+
57
+ await searchReddit({
58
+ query: "test",
59
+ subreddit: "programming",
60
+ sort: "top",
61
+ time_filter: "week",
62
+ limit: 5,
63
+ type: "link",
64
+ })
65
+
66
+ expect(mockRedditClient.searchReddit).toHaveBeenCalledWith("test", {
67
+ subreddit: "programming",
68
+ sort: "top",
69
+ timeFilter: "week",
70
+ limit: 5,
71
+ type: "link",
72
+ })
73
+ })
74
+
75
+ it("should throw error if Reddit client is not initialized", async () => {
76
+ vi.mocked(getRedditClient).mockReturnValue(null)
77
+
78
+ await expect(searchReddit({ query: "test" })).rejects.toThrow(
79
+ new McpError(ErrorCode.InternalError, "Reddit client not initialized"),
80
+ )
81
+ })
82
+
83
+ it("should throw error if query is empty", async () => {
84
+ await expect(searchReddit({ query: "" })).rejects.toThrow(
85
+ new McpError(ErrorCode.InvalidParams, "Search query cannot be empty"),
86
+ )
87
+
88
+ await expect(searchReddit({ query: " " })).rejects.toThrow(
89
+ new McpError(ErrorCode.InvalidParams, "Search query cannot be empty"),
90
+ )
91
+ })
92
+
93
+ it("should handle search errors", async () => {
94
+ mockRedditClient.searchReddit.mockRejectedValue(new Error("API Error"))
95
+
96
+ await expect(searchReddit({ query: "test" })).rejects.toThrow(
97
+ new McpError(ErrorCode.InternalError, "Failed to search Reddit: Error: API Error"),
98
+ )
99
+ })
100
+ })