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.
package/src/index.ts DELETED
@@ -1,518 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js"
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js"
4
- import { initializeRedditClient } from "./client/reddit-client"
5
- import * as tools from "./tools"
6
- import dotenv from "dotenv"
7
-
8
- // Load environment variables
9
- dotenv.config()
10
-
11
- class RedditServer {
12
- private server: Server
13
-
14
- constructor() {
15
- this.server = new Server(
16
- {
17
- name: "reddit-mcp-server",
18
- version: "0.1.0",
19
- },
20
- {
21
- capabilities: {
22
- tools: {},
23
- },
24
- },
25
- )
26
-
27
- // Initialize the Reddit client
28
- this.initializeRedditClient()
29
-
30
- this.setupToolHandlers()
31
-
32
- this.server.onerror = async (error) => {
33
- await this.server.sendLoggingMessage({
34
- level: "error",
35
- logger: "reddit-server",
36
- data: `Server error: ${error}`,
37
- })
38
- }
39
-
40
- process.on("SIGINT", async () => {
41
- await this.server.close()
42
- process.exit(0)
43
- })
44
- }
45
-
46
- private initializeRedditClient() {
47
- const clientId = process.env.REDDIT_CLIENT_ID
48
- const clientSecret = process.env.REDDIT_CLIENT_SECRET
49
- const userAgent = process.env.REDDIT_USER_AGENT || "RedditMCPServer/0.1.0"
50
- const username = process.env.REDDIT_USERNAME
51
- const password = process.env.REDDIT_PASSWORD
52
-
53
- if (!clientId || !clientSecret) {
54
- // Can't use server logging here as server isn't initialized yet
55
- // Exit silently with error code
56
- process.exit(1)
57
- }
58
-
59
- try {
60
- initializeRedditClient({
61
- clientId,
62
- clientSecret,
63
- userAgent,
64
- username,
65
- password,
66
- })
67
-
68
- // Client initialized successfully
69
- } catch {
70
- // Can't use server logging here as server isn't connected yet
71
- // Exit silently with error code
72
- process.exit(1)
73
- }
74
- }
75
-
76
- private setupToolHandlers() {
77
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
78
- tools: [
79
- {
80
- name: "test_reddit_mcp_server",
81
- description: "Test the Reddit MCP Server",
82
- inputSchema: {
83
- type: "object",
84
- properties: {
85
- // No input parameters, this will just return a test message
86
- },
87
- },
88
- },
89
- {
90
- name: "get_reddit_post",
91
- description: "Get a Reddit post",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- subreddit: {
96
- type: "string",
97
- description: "The subreddit to fetch posts from",
98
- },
99
- post_id: {
100
- type: "string",
101
- description: "The ID of the post to fetch",
102
- },
103
- },
104
- required: ["subreddit", "post_id"],
105
- },
106
- },
107
- {
108
- name: "get_top_posts",
109
- description: "Get top posts from a subreddit",
110
- inputSchema: {
111
- type: "object",
112
- properties: {
113
- subreddit: {
114
- type: "string",
115
- description: "Name of the subreddit",
116
- },
117
- time_filter: {
118
- type: "string",
119
- description: "Time period to filter posts (e.g. 'day', 'week', 'month', 'year', 'all')",
120
- enum: ["day", "week", "month", "year", "all"],
121
- default: "week",
122
- },
123
- limit: {
124
- type: "integer",
125
- description: "Number of posts to fetch",
126
- default: 10,
127
- },
128
- },
129
- required: ["subreddit"],
130
- },
131
- },
132
- {
133
- name: "get_user_info",
134
- description: "Get information about a Reddit user",
135
- inputSchema: {
136
- type: "object",
137
- properties: {
138
- username: {
139
- type: "string",
140
- description: "The username of the Reddit user to get info for",
141
- },
142
- },
143
- required: ["username"],
144
- },
145
- },
146
- {
147
- name: "get_subreddit_info",
148
- description: "Get information about a subreddit",
149
- inputSchema: {
150
- type: "object",
151
- properties: {
152
- subreddit_name: {
153
- type: "string",
154
- description: "Name of the subreddit",
155
- },
156
- },
157
- required: ["subreddit_name"],
158
- },
159
- },
160
- {
161
- name: "get_trending_subreddits",
162
- description: "Get currently trending subreddits",
163
- inputSchema: {
164
- type: "object",
165
- properties: {},
166
- },
167
- },
168
- {
169
- name: "create_post",
170
- description: "Create a new post in a subreddit",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- subreddit: {
175
- type: "string",
176
- description: "Name of the subreddit to post in",
177
- },
178
- title: {
179
- type: "string",
180
- description: "Title of the post",
181
- },
182
- content: {
183
- type: "string",
184
- description: "Content of the post (text for self posts, URL for link posts)",
185
- },
186
- is_self: {
187
- type: "boolean",
188
- description: "Whether this is a self (text) post (true) or link post (false)",
189
- default: true,
190
- },
191
- },
192
- required: ["subreddit", "title", "content"],
193
- },
194
- },
195
- {
196
- name: "reply_to_post",
197
- description: "Post a reply to an existing Reddit post",
198
- inputSchema: {
199
- type: "object",
200
- properties: {
201
- post_id: {
202
- type: "string",
203
- description: "The ID of the post to reply to",
204
- },
205
- content: {
206
- type: "string",
207
- description: "The content of the reply",
208
- },
209
- subreddit: {
210
- type: "string",
211
- description: "The subreddit name if known (for validation)",
212
- },
213
- },
214
- required: ["post_id", "content"],
215
- },
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
- },
357
- ],
358
- }))
359
-
360
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
361
- try {
362
- const toolName = request.params.name
363
- const toolParams = request.params.arguments || {}
364
-
365
- // Log tool call
366
- await this.server.sendLoggingMessage({
367
- level: "debug",
368
- logger: "reddit-server",
369
- data: `Tool call: ${toolName}`,
370
- })
371
-
372
- switch (toolName) {
373
- case "test_reddit_mcp_server":
374
- return {
375
- content: [
376
- {
377
- type: "text",
378
- text: "Hello, world! The Reddit MCP Server is working correctly.",
379
- },
380
- ],
381
- }
382
-
383
- case "get_reddit_post":
384
- return await tools.getRedditPost(toolParams as { subreddit: string; post_id: string })
385
-
386
- case "get_top_posts":
387
- return await tools.getTopPosts(
388
- toolParams as {
389
- subreddit: string
390
- time_filter?: string
391
- limit?: number
392
- },
393
- )
394
-
395
- case "get_user_info":
396
- return await tools.getUserInfo(toolParams as { username: string })
397
-
398
- case "get_subreddit_info":
399
- return await tools.getSubredditInfo(toolParams as { subreddit_name: string })
400
-
401
- case "get_trending_subreddits":
402
- return await tools.getTrendingSubreddits()
403
-
404
- case "create_post":
405
- return await tools.createPost(
406
- toolParams as {
407
- subreddit: string
408
- title: string
409
- content: string
410
- is_self?: boolean
411
- },
412
- )
413
-
414
- case "reply_to_post":
415
- return await tools.replyToPost(
416
- toolParams as {
417
- post_id: string
418
- content: string
419
- subreddit?: string
420
- },
421
- )
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
-
465
- default:
466
- throw new McpError(ErrorCode.MethodNotFound, `Tool with name ${toolName} not found`)
467
- }
468
- } catch (error: unknown) {
469
- if (error instanceof Error) {
470
- await this.server.sendLoggingMessage({
471
- level: "error",
472
- logger: "reddit-server",
473
- data: `Error calling tool: ${error.message}`,
474
- })
475
-
476
- throw new McpError(ErrorCode.InternalError, `Failed to fetch data: ${error.message}`)
477
- }
478
-
479
- throw error
480
- }
481
- })
482
- }
483
-
484
- async run() {
485
- const transport = new StdioServerTransport()
486
- await this.server.connect(transport)
487
-
488
- // Log server startup
489
- await this.server.sendLoggingMessage({
490
- level: "info",
491
- logger: "reddit-server",
492
- data: "Reddit MCP Server is running",
493
- })
494
-
495
- // Log authentication status
496
- const username = process.env.REDDIT_USERNAME
497
- const password = process.env.REDDIT_PASSWORD
498
- await this.server.sendLoggingMessage({
499
- level: "info",
500
- logger: "reddit-server",
501
- data:
502
- username && password
503
- ? `Authenticated as user: ${username}`
504
- : "Running in read-only mode (no user authentication)",
505
- })
506
- }
507
- }
508
-
509
- // Only run if this is the main module
510
- if (require.main === module) {
511
- const server = new RedditServer()
512
- server.run().catch(() => {
513
- // Exit silently on error
514
- process.exit(1)
515
- })
516
- }
517
-
518
- export { RedditServer }
@@ -1,119 +0,0 @@
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
- })