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.
@@ -0,0 +1,258 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { getUserInfo, getUserPosts, getUserComments } from "../user-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("user-tools", () => {
9
+ const mockRedditClient = {
10
+ getUser: vi.fn(),
11
+ getUserPosts: vi.fn(),
12
+ getUserComments: vi.fn(),
13
+ }
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks()
17
+ vi.mocked(getRedditClient).mockReturnValue(mockRedditClient as any)
18
+ })
19
+
20
+ describe("getUserInfo", () => {
21
+ it("should fetch and format user information", async () => {
22
+ const mockUser = {
23
+ name: "testuser",
24
+ id: "user123",
25
+ commentKarma: 5000,
26
+ linkKarma: 1000,
27
+ totalKarma: 6000,
28
+ isMod: true,
29
+ isGold: false,
30
+ isEmployee: false,
31
+ createdUtc: 1600000000,
32
+ profileUrl: "https://reddit.com/user/testuser",
33
+ }
34
+
35
+ mockRedditClient.getUser.mockResolvedValue(mockUser)
36
+
37
+ const result = await getUserInfo({ username: "testuser" })
38
+
39
+ expect(mockRedditClient.getUser).toHaveBeenCalledWith("testuser")
40
+ expect(result.content).toHaveLength(1)
41
+ expect(result.content[0].type).toBe("text")
42
+ expect(result.content[0].text).toContain("User Information: u/testuser")
43
+ expect(result.content[0].text).toContain("Comment Karma: 5,000")
44
+ expect(result.content[0].text).toContain("Moderator")
45
+ })
46
+
47
+ it("should throw error if Reddit client is not initialized", async () => {
48
+ vi.mocked(getRedditClient).mockReturnValue(null)
49
+
50
+ await expect(getUserInfo({ username: "test" })).rejects.toThrow(
51
+ new McpError(ErrorCode.InternalError, "Reddit client not initialized"),
52
+ )
53
+ })
54
+
55
+ it("should handle API errors", async () => {
56
+ mockRedditClient.getUser.mockRejectedValue(new Error("User not found"))
57
+
58
+ await expect(getUserInfo({ username: "nonexistent" })).rejects.toThrow(
59
+ new McpError(ErrorCode.InternalError, "Failed to fetch user data: Error: User not found"),
60
+ )
61
+ })
62
+ })
63
+
64
+ describe("getUserPosts", () => {
65
+ it("should fetch user posts with default parameters", async () => {
66
+ const mockPosts = [
67
+ {
68
+ id: "post1",
69
+ title: "My First Post",
70
+ author: "testuser",
71
+ subreddit: "programming",
72
+ score: 100,
73
+ upvoteRatio: 0.95,
74
+ numComments: 20,
75
+ createdUtc: 1700000000,
76
+ selftext: "This is my post content",
77
+ permalink: "/r/programming/comments/post1",
78
+ over18: false,
79
+ spoiler: false,
80
+ },
81
+ ]
82
+
83
+ mockRedditClient.getUserPosts.mockResolvedValue(mockPosts)
84
+
85
+ const result = await getUserPosts({ username: "testuser" })
86
+
87
+ expect(mockRedditClient.getUserPosts).toHaveBeenCalledWith("testuser", {
88
+ sort: "new",
89
+ timeFilter: "all",
90
+ limit: 10,
91
+ })
92
+
93
+ expect(result.content).toHaveLength(1)
94
+ expect(result.content[0].type).toBe("text")
95
+ expect(result.content[0].text).toContain("Posts by u/testuser")
96
+ expect(result.content[0].text).toContain("My First Post")
97
+ expect(result.content[0].text).toContain("Subreddit: r/programming")
98
+ })
99
+
100
+ it("should fetch posts with custom parameters", async () => {
101
+ mockRedditClient.getUserPosts.mockResolvedValue([])
102
+
103
+ await getUserPosts({
104
+ username: "testuser",
105
+ sort: "top",
106
+ time_filter: "week",
107
+ limit: 25,
108
+ })
109
+
110
+ expect(mockRedditClient.getUserPosts).toHaveBeenCalledWith("testuser", {
111
+ sort: "top",
112
+ timeFilter: "week",
113
+ limit: 25,
114
+ })
115
+ })
116
+
117
+ it("should handle NSFW and spoiler posts", async () => {
118
+ const mockPosts = [
119
+ {
120
+ id: "post2",
121
+ title: "NSFW Post",
122
+ author: "testuser",
123
+ subreddit: "test",
124
+ score: 50,
125
+ upvoteRatio: 0.8,
126
+ numComments: 5,
127
+ createdUtc: 1700000000,
128
+ selftext: "",
129
+ permalink: "/r/test/comments/post2",
130
+ over18: true,
131
+ spoiler: true,
132
+ },
133
+ ]
134
+
135
+ mockRedditClient.getUserPosts.mockResolvedValue(mockPosts)
136
+
137
+ const result = await getUserPosts({ username: "testuser" })
138
+
139
+ expect(result.content[0].text).toContain("**NSFW**")
140
+ expect(result.content[0].text).toContain("**Spoiler**")
141
+ })
142
+
143
+ it("should throw error if Reddit client is not initialized", async () => {
144
+ vi.mocked(getRedditClient).mockReturnValue(null)
145
+
146
+ await expect(getUserPosts({ username: "test" })).rejects.toThrow(
147
+ new McpError(ErrorCode.InternalError, "Reddit client not initialized"),
148
+ )
149
+ })
150
+
151
+ it("should handle API errors", async () => {
152
+ mockRedditClient.getUserPosts.mockRejectedValue(new Error("API Error"))
153
+
154
+ await expect(getUserPosts({ username: "test" })).rejects.toThrow(
155
+ new McpError(ErrorCode.InternalError, "Failed to fetch user posts: Error: API Error"),
156
+ )
157
+ })
158
+ })
159
+
160
+ describe("getUserComments", () => {
161
+ it("should fetch user comments with default parameters", async () => {
162
+ const mockComments = [
163
+ {
164
+ id: "comment1",
165
+ author: "testuser",
166
+ body: "This is my insightful comment",
167
+ score: 42,
168
+ subreddit: "programming",
169
+ submissionTitle: "Cool Programming Topic",
170
+ createdUtc: 1700000000,
171
+ edited: false,
172
+ isSubmitter: false,
173
+ permalink: "/r/programming/comments/xyz/cool_programming_topic/comment1",
174
+ },
175
+ ]
176
+
177
+ mockRedditClient.getUserComments.mockResolvedValue(mockComments)
178
+
179
+ const result = await getUserComments({ username: "testuser" })
180
+
181
+ expect(mockRedditClient.getUserComments).toHaveBeenCalledWith("testuser", {
182
+ sort: "new",
183
+ timeFilter: "all",
184
+ limit: 10,
185
+ })
186
+
187
+ expect(result.content).toHaveLength(1)
188
+ expect(result.content[0].type).toBe("text")
189
+ expect(result.content[0].text).toContain("Comments by u/testuser")
190
+ expect(result.content[0].text).toContain('In r/programming on "Cool Programming Topic"')
191
+ expect(result.content[0].text).toContain("This is my insightful comment")
192
+ })
193
+
194
+ it("should handle edited comments", async () => {
195
+ const mockComments = [
196
+ {
197
+ id: "comment2",
198
+ author: "testuser",
199
+ body: "Edited comment text",
200
+ score: 10,
201
+ subreddit: "test",
202
+ submissionTitle: "Test Post",
203
+ createdUtc: 1700000000,
204
+ edited: true,
205
+ isSubmitter: false,
206
+ permalink: "/r/test/comments/abc/test_post/comment2",
207
+ },
208
+ ]
209
+
210
+ mockRedditClient.getUserComments.mockResolvedValue(mockComments)
211
+
212
+ const result = await getUserComments({ username: "testuser" })
213
+
214
+ expect(result.content[0].text).toContain("*(edited)*")
215
+ })
216
+
217
+ it("should truncate long comments", async () => {
218
+ const longComment = "x".repeat(350)
219
+ const mockComments = [
220
+ {
221
+ id: "comment3",
222
+ author: "testuser",
223
+ body: longComment,
224
+ score: 5,
225
+ subreddit: "test",
226
+ submissionTitle: "Test",
227
+ createdUtc: 1700000000,
228
+ edited: false,
229
+ isSubmitter: false,
230
+ permalink: "/r/test/comments/def/test/comment3",
231
+ },
232
+ ]
233
+
234
+ mockRedditClient.getUserComments.mockResolvedValue(mockComments)
235
+
236
+ const result = await getUserComments({ username: "testuser" })
237
+
238
+ expect(result.content[0].text).toContain("x".repeat(300) + "...")
239
+ expect(result.content[0].text).not.toContain("x".repeat(301))
240
+ })
241
+
242
+ it("should throw error if Reddit client is not initialized", async () => {
243
+ vi.mocked(getRedditClient).mockReturnValue(null)
244
+
245
+ await expect(getUserComments({ username: "test" })).rejects.toThrow(
246
+ new McpError(ErrorCode.InternalError, "Reddit client not initialized"),
247
+ )
248
+ })
249
+
250
+ it("should handle API errors", async () => {
251
+ mockRedditClient.getUserComments.mockRejectedValue(new Error("API Error"))
252
+
253
+ await expect(getUserComments({ username: "test" })).rejects.toThrow(
254
+ new McpError(ErrorCode.InternalError, "Failed to fetch user comments: Error: API Error"),
255
+ )
256
+ })
257
+ })
258
+ })
@@ -0,0 +1,61 @@
1
+ import { getRedditClient } from "../client/reddit-client"
2
+ import { formatPost } from "../utils/formatters"
3
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"
4
+
5
+ export async function getPostComments(params: { post_id: string; subreddit: string; sort?: string; limit?: number }) {
6
+ const { post_id, subreddit, sort = "best", limit = 100 } = params
7
+ const client = getRedditClient()
8
+
9
+ if (!client) {
10
+ throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
11
+ }
12
+
13
+ if (!post_id || !subreddit) {
14
+ throw new McpError(ErrorCode.InvalidParams, "post_id and subreddit are required")
15
+ }
16
+
17
+ try {
18
+ const { post, comments } = await client.getPostComments(post_id, subreddit, {
19
+ sort,
20
+ limit,
21
+ })
22
+
23
+ const formattedPost = formatPost(post)
24
+
25
+ // Function to format comments with proper indentation
26
+ const formatComment = (comment: any): string => {
27
+ const edited = comment.edited ? " *(edited)*" : ""
28
+ const submitter = comment.isSubmitter ? " **[OP]**" : ""
29
+ const depth = comment.depth || 0
30
+ const prefix = " ".repeat(depth) + (depth > 0 ? "└─ " : "")
31
+
32
+ return `${prefix}**u/${comment.author}**${submitter} • ${comment.score} points • ${new Date(comment.createdUtc * 1000).toLocaleString()}${edited}
33
+ ${prefix}${comment.body.split("\n").join(`\n${prefix}`)}`
34
+ }
35
+
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `# Comments for: ${formattedPost.title}
41
+
42
+ ## Post Details
43
+ - Author: u/${formattedPost.author}
44
+ - Subreddit: r/${formattedPost.subreddit}
45
+ - Score: ${formattedPost.score} (${formattedPost.upvoteRatio}% upvoted)
46
+ - Posted: ${formattedPost.createdAt}
47
+ - Link: https://reddit.com${formattedPost.permalink}
48
+
49
+ ## Post Content
50
+ ${formattedPost.selftext || "[Link post - no text content]"}
51
+
52
+ ## Comments (${comments.length} loaded, sorted by ${sort})
53
+
54
+ ${comments.map((comment) => formatComment(comment)).join("\n\n---\n\n")}`,
55
+ },
56
+ ],
57
+ }
58
+ } catch (error) {
59
+ throw new McpError(ErrorCode.InternalError, `Failed to fetch comments: ${String(error)}`)
60
+ }
61
+ }
@@ -1,3 +1,5 @@
1
1
  export * from "./user-tools"
2
2
  export * from "./post-tools"
3
3
  export * from "./subreddit-tools"
4
+ export * from "./search-tools"
5
+ export * from "./comment-tools"
@@ -0,0 +1,67 @@
1
+ import { getRedditClient } from "../client/reddit-client"
2
+ import { formatPost } from "../utils/formatters"
3
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"
4
+
5
+ export async function searchReddit(params: {
6
+ query: string
7
+ subreddit?: string
8
+ sort?: string
9
+ time_filter?: string
10
+ limit?: number
11
+ type?: string
12
+ }) {
13
+ const { query, subreddit, sort = "relevance", time_filter = "all", limit = 10, type = "link" } = params
14
+ const client = getRedditClient()
15
+
16
+ if (!client) {
17
+ throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
18
+ }
19
+
20
+ if (!query || query.trim().length === 0) {
21
+ throw new McpError(ErrorCode.InvalidParams, "Search query cannot be empty")
22
+ }
23
+
24
+ try {
25
+ const posts = await client.searchReddit(query, {
26
+ subreddit,
27
+ sort,
28
+ timeFilter: time_filter,
29
+ limit,
30
+ type,
31
+ })
32
+
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `# Reddit Search Results for: "${query}"${subreddit ? ` in r/${subreddit}` : ""}
38
+
39
+ ## Search Parameters
40
+ - Sort: ${sort}
41
+ - Time Filter: ${time_filter}
42
+ - Type: ${type}
43
+ - Results: ${posts.length}
44
+
45
+ ${posts
46
+ .map((post, index) => {
47
+ const formatted = formatPost(post)
48
+ return `### ${index + 1}. ${formatted.title}
49
+ - Author: u/${formatted.author}
50
+ - Subreddit: r/${formatted.subreddit}
51
+ - Score: ${formatted.score} (${formatted.upvoteRatio}% upvoted)
52
+ - Comments: ${formatted.numComments}
53
+ - Posted: ${formatted.createdAt}
54
+ ${formatted.selftext ? `\n${formatted.selftext.substring(0, 200)}${formatted.selftext.length > 200 ? "..." : ""}\n` : ""}
55
+ - Link: https://reddit.com${formatted.permalink}
56
+ ${formatted.nsfw ? "- **NSFW**" : ""}
57
+ ${formatted.spoiler ? "- **Spoiler**" : ""}
58
+ `
59
+ })
60
+ .join("\n")}`,
61
+ },
62
+ ],
63
+ }
64
+ } catch (error) {
65
+ throw new McpError(ErrorCode.InternalError, `Failed to search Reddit: ${String(error)}`)
66
+ }
67
+ }
@@ -46,3 +46,102 @@ export async function getUserInfo(params: { username: string }) {
46
46
  throw new McpError(ErrorCode.InternalError, `Failed to fetch user data: ${String(error)}`)
47
47
  }
48
48
  }
49
+
50
+ export async function getUserPosts(params: { username: string; sort?: string; time_filter?: string; limit?: number }) {
51
+ const { username, sort = "new", time_filter = "all", limit = 10 } = params
52
+ const client = getRedditClient()
53
+
54
+ if (!client) {
55
+ throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
56
+ }
57
+
58
+ try {
59
+ const posts = await client.getUserPosts(username, {
60
+ sort,
61
+ timeFilter: time_filter,
62
+ limit,
63
+ })
64
+
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: `# Posts by u/${username}
70
+
71
+ ## Sort: ${sort} | Time: ${time_filter} | Count: ${posts.length}
72
+
73
+ ${posts
74
+ .map((post, index) => {
75
+ const date = new Date(post.createdUtc * 1000).toLocaleString()
76
+ const selftext = post.selftext
77
+ ? `\n${post.selftext.substring(0, 200)}${post.selftext.length > 200 ? "..." : ""}\n`
78
+ : ""
79
+
80
+ return `### ${index + 1}. ${post.title}
81
+ - Subreddit: r/${post.subreddit}
82
+ - Score: ${post.score} (${Math.round(post.upvoteRatio * 100)}% upvoted)
83
+ - Comments: ${post.numComments}
84
+ - Posted: ${date}
85
+ ${selftext}
86
+ - Link: https://reddit.com${post.permalink}
87
+ ${post.over18 ? "- **NSFW**" : ""}
88
+ ${post.spoiler ? "- **Spoiler**" : ""}`
89
+ })
90
+ .join("\n\n---\n\n")}`,
91
+ },
92
+ ],
93
+ }
94
+ } catch (error) {
95
+ throw new McpError(ErrorCode.InternalError, `Failed to fetch user posts: ${String(error)}`)
96
+ }
97
+ }
98
+
99
+ export async function getUserComments(params: {
100
+ username: string
101
+ sort?: string
102
+ time_filter?: string
103
+ limit?: number
104
+ }) {
105
+ const { username, sort = "new", time_filter = "all", limit = 10 } = params
106
+ const client = getRedditClient()
107
+
108
+ if (!client) {
109
+ throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
110
+ }
111
+
112
+ try {
113
+ const comments = await client.getUserComments(username, {
114
+ sort,
115
+ timeFilter: time_filter,
116
+ limit,
117
+ })
118
+
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: `# Comments by u/${username}
124
+
125
+ ## Sort: ${sort} | Time: ${time_filter} | Count: ${comments.length}
126
+
127
+ ${comments
128
+ .map((comment, index) => {
129
+ const date = new Date(comment.createdUtc * 1000).toLocaleString()
130
+ const edited = comment.edited ? " *(edited)*" : ""
131
+ const body = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body
132
+
133
+ return `### ${index + 1}. In r/${comment.subreddit} on "${comment.submissionTitle}"
134
+ - Score: ${comment.score} points
135
+ - Posted: ${date}${edited}
136
+ - Link: https://reddit.com${comment.permalink}
137
+
138
+ ${body}`
139
+ })
140
+ .join("\n\n---\n\n")}`,
141
+ },
142
+ ],
143
+ }
144
+ } catch (error) {
145
+ throw new McpError(ErrorCode.InternalError, `Failed to fetch user comments: ${String(error)}`)
146
+ }
147
+ }
package/src/types.ts CHANGED
@@ -50,6 +50,8 @@ export interface RedditComment {
50
50
  edited: boolean
51
51
  isSubmitter: boolean
52
52
  permalink: string
53
+ depth?: number
54
+ parentId?: string
53
55
  }
54
56
 
55
57
  export interface RedditSubreddit {
@@ -297,3 +297,20 @@ export function formatCommentInfo(comment: RedditComment): FormattedCommentInfo
297
297
  commentAnalysis: analyzeCommentImpact(comment.score, comment.edited, comment.isSubmitter),
298
298
  }
299
299
  }
300
+
301
+ // Simple formatter for posts (used in search and comment tools)
302
+ export function formatPost(post: RedditPost) {
303
+ return {
304
+ title: post.title,
305
+ author: post.author,
306
+ subreddit: post.subreddit,
307
+ score: post.score,
308
+ upvoteRatio: Math.round(post.upvoteRatio * 100),
309
+ numComments: post.numComments,
310
+ createdAt: formatTimestamp(post.createdUtc),
311
+ selftext: post.selftext,
312
+ permalink: post.permalink,
313
+ nsfw: post.over18,
314
+ spoiler: post.spoiler,
315
+ }
316
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ exclude: [
11
+ 'node_modules/',
12
+ 'dist/',
13
+ '**/*.d.ts',
14
+ '**/*.config.*',
15
+ '**/mockData.ts',
16
+ '**/bin.ts',
17
+ ]
18
+ },
19
+ },
20
+ })