reddit-mcp-server 1.0.3 → 1.0.5

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
@@ -36,7 +36,7 @@ class RedditServer {
36
36
  data: `Server error: ${error}`,
37
37
  })
38
38
  }
39
-
39
+
40
40
  process.on("SIGINT", async () => {
41
41
  await this.server.close()
42
42
  process.exit(0)
@@ -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
  }
@@ -305,23 +484,24 @@ class RedditServer {
305
484
  async run() {
306
485
  const transport = new StdioServerTransport()
307
486
  await this.server.connect(transport)
308
-
487
+
309
488
  // Log server startup
310
489
  await this.server.sendLoggingMessage({
311
490
  level: "info",
312
491
  logger: "reddit-server",
313
492
  data: "Reddit MCP Server is running",
314
493
  })
315
-
494
+
316
495
  // Log authentication status
317
496
  const username = process.env.REDDIT_USERNAME
318
497
  const password = process.env.REDDIT_PASSWORD
319
498
  await this.server.sendLoggingMessage({
320
499
  level: "info",
321
500
  logger: "reddit-server",
322
- data: username && password
323
- ? `Authenticated as user: ${username}`
324
- : "Running in read-only mode (no user authentication)",
501
+ data:
502
+ username && password
503
+ ? `Authenticated as user: ${username}`
504
+ : "Running in read-only mode (no user authentication)",
325
505
  })
326
506
  }
327
507
  }
@@ -329,7 +509,10 @@ class RedditServer {
329
509
  // Only run if this is the main module
330
510
  if (require.main === module) {
331
511
  const server = new RedditServer()
332
- server.run().catch(console.error)
512
+ server.run().catch(() => {
513
+ // Exit silently on error
514
+ process.exit(1)
515
+ })
333
516
  }
334
517
 
335
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
+ })