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/.claude/settings.local.json +13 -1
- package/LICENSE +2 -2
- package/ROADMAP.md +77 -0
- package/dist/bin.js +652 -68
- package/dist/index.js +649 -67
- package/eslint.config.js +2 -0
- package/package.json +10 -7
- package/src/bin.ts +3 -1
- package/src/client/__tests__/reddit-client.test.ts +478 -0
- package/src/client/reddit-client.ts +351 -53
- package/src/index.ts +189 -7
- package/src/tools/__tests__/comment-tools.test.ts +119 -0
- package/src/tools/__tests__/search-tools.test.ts +100 -0
- package/src/tools/__tests__/user-tools.test.ts +258 -0
- package/src/tools/comment-tools.ts +61 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/search-tools.ts +67 -0
- package/src/tools/user-tools.ts +99 -0
- package/src/types.ts +2 -0
- package/src/utils/formatters.ts +17 -0
- package/vitest.config.ts +20 -0
|
@@ -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
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tools/user-tools.ts
CHANGED
|
@@ -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
package/src/utils/formatters.ts
CHANGED
|
@@ -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
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
})
|