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/CHANGELOG.md +34 -0
- package/package.json +7 -1
- package/.claude/settings.local.json +0 -35
- package/.env.example +0 -7
- package/.prettierignore +0 -7
- package/.prettierrc +0 -8
- package/CLAUDE.md +0 -104
- package/Dockerfile +0 -17
- package/ROADMAP.md +0 -77
- package/eslint.config.js +0 -76
- package/pnpm-workspace.yaml +0 -2
- package/smithery.yaml +0 -37
- package/src/bin.ts +0 -7
- package/src/client/__tests__/reddit-client.test.ts +0 -478
- package/src/client/reddit-client.ts +0 -638
- package/src/index.ts +0 -518
- package/src/tools/__tests__/comment-tools.test.ts +0 -119
- package/src/tools/__tests__/search-tools.test.ts +0 -100
- package/src/tools/__tests__/user-tools.test.ts +0 -258
- package/src/tools/comment-tools.ts +0 -61
- package/src/tools/index.ts +0 -5
- package/src/tools/post-tools.ts +0 -178
- package/src/tools/search-tools.ts +0 -67
- package/src/tools/subreddit-tools.ts +0 -91
- package/src/tools/user-tools.ts +0 -147
- package/src/types.ts +0 -148
- package/src/utils/formatters.ts +0 -316
- package/tsconfig.json +0 -19
- package/tsup.config.ts +0 -12
- package/vitest.config.ts +0 -20
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,258 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,61 +0,0 @@
|
|
|
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
DELETED
package/src/tools/post-tools.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { getRedditClient } from "../client/reddit-client"
|
|
2
|
-
import { formatPostInfo, formatCommentInfo } from "../utils/formatters"
|
|
3
|
-
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"
|
|
4
|
-
|
|
5
|
-
export async function getRedditPost(params: { subreddit: string; post_id: string }) {
|
|
6
|
-
const { subreddit, post_id } = params
|
|
7
|
-
const client = getRedditClient()
|
|
8
|
-
|
|
9
|
-
if (!client) {
|
|
10
|
-
throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
// Getting post
|
|
15
|
-
const post = await client.getPost(post_id, subreddit)
|
|
16
|
-
const formattedPost = formatPostInfo(post)
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
content: [
|
|
20
|
-
{
|
|
21
|
-
type: "text",
|
|
22
|
-
text: `
|
|
23
|
-
# Post from r/${formattedPost.subreddit}
|
|
24
|
-
|
|
25
|
-
## Post Details
|
|
26
|
-
- Title: ${formattedPost.title}
|
|
27
|
-
- Type: ${formattedPost.type}
|
|
28
|
-
- Author: u/${formattedPost.author}
|
|
29
|
-
|
|
30
|
-
## Content
|
|
31
|
-
${formattedPost.content}
|
|
32
|
-
|
|
33
|
-
## Stats
|
|
34
|
-
- Score: ${formattedPost.stats.score.toLocaleString()}
|
|
35
|
-
- Upvote Ratio: ${(formattedPost.stats.upvoteRatio * 100).toFixed(1)}%
|
|
36
|
-
- Comments: ${formattedPost.stats.comments.toLocaleString()}
|
|
37
|
-
|
|
38
|
-
## Metadata
|
|
39
|
-
- Posted: ${formattedPost.metadata.posted}
|
|
40
|
-
- Flags: ${formattedPost.metadata.flags.length ? formattedPost.metadata.flags.join(", ") : "None"}
|
|
41
|
-
- Flair: ${formattedPost.metadata.flair}
|
|
42
|
-
|
|
43
|
-
## Links
|
|
44
|
-
- Full Post: ${formattedPost.links.fullPost}
|
|
45
|
-
- Short Link: ${formattedPost.links.shortLink}
|
|
46
|
-
|
|
47
|
-
## Engagement Analysis
|
|
48
|
-
- ${formattedPost.engagementAnalysis.replace(/\n - /g, "\n- ")}
|
|
49
|
-
|
|
50
|
-
## Best Time to Engage
|
|
51
|
-
${formattedPost.bestTimeToEngage}
|
|
52
|
-
`,
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
}
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// Error will be logged by the server
|
|
58
|
-
throw new McpError(ErrorCode.InternalError, `Failed to fetch post data: ${String(error)}`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function getTopPosts(params: { subreddit: string; time_filter?: string; limit?: number }) {
|
|
63
|
-
const { subreddit, time_filter = "week", limit = 10 } = params
|
|
64
|
-
const client = getRedditClient()
|
|
65
|
-
|
|
66
|
-
if (!client) {
|
|
67
|
-
throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
// Getting top posts
|
|
72
|
-
const posts = await client.getTopPosts(subreddit, time_filter, limit)
|
|
73
|
-
const formattedPosts = posts.map(formatPostInfo)
|
|
74
|
-
|
|
75
|
-
const postSummaries = formattedPosts
|
|
76
|
-
.map(
|
|
77
|
-
(post, index) => `
|
|
78
|
-
### ${index + 1}. ${post.title}
|
|
79
|
-
- Author: u/${post.author}
|
|
80
|
-
- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
81
|
-
- Comments: ${post.stats.comments.toLocaleString()}
|
|
82
|
-
- Posted: ${post.metadata.posted}
|
|
83
|
-
- Link: ${post.links.shortLink}
|
|
84
|
-
`,
|
|
85
|
-
)
|
|
86
|
-
.join("\n")
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: "text",
|
|
92
|
-
text: `
|
|
93
|
-
# Top Posts from r/${subreddit} (${time_filter})
|
|
94
|
-
|
|
95
|
-
${postSummaries}
|
|
96
|
-
`,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
// Error will be logged by the server
|
|
102
|
-
throw new McpError(ErrorCode.InternalError, `Failed to fetch top posts: ${String(error)}`)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function createPost(params: { subreddit: string; title: string; content: string; is_self?: boolean }) {
|
|
107
|
-
const { subreddit, title, content, is_self = true } = params
|
|
108
|
-
const client = getRedditClient()
|
|
109
|
-
|
|
110
|
-
if (!client) {
|
|
111
|
-
throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
// Creating post
|
|
116
|
-
const post = await client.createPost(subreddit, title, content, is_self)
|
|
117
|
-
const formattedPost = formatPostInfo(post)
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
content: [
|
|
121
|
-
{
|
|
122
|
-
type: "text",
|
|
123
|
-
text: `
|
|
124
|
-
# Post Created Successfully
|
|
125
|
-
|
|
126
|
-
## Post Details
|
|
127
|
-
- Title: ${formattedPost.title}
|
|
128
|
-
- Subreddit: r/${formattedPost.subreddit}
|
|
129
|
-
- Type: ${formattedPost.type}
|
|
130
|
-
- Link: ${formattedPost.links.fullPost}
|
|
131
|
-
|
|
132
|
-
Your post has been successfully submitted to r/${formattedPost.subreddit}.
|
|
133
|
-
`,
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
}
|
|
137
|
-
} catch (error) {
|
|
138
|
-
// Error will be logged by the server
|
|
139
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create post: ${String(error)}`)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function replyToPost(params: { post_id: string; content: string; subreddit?: string }) {
|
|
144
|
-
const { post_id, content } = params
|
|
145
|
-
const client = getRedditClient()
|
|
146
|
-
|
|
147
|
-
if (!client) {
|
|
148
|
-
throw new McpError(ErrorCode.InternalError, "Reddit client not initialized")
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
// Replying to post
|
|
153
|
-
const comment = await client.replyToPost(post_id, content)
|
|
154
|
-
const formattedComment = formatCommentInfo(comment)
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
content: [
|
|
158
|
-
{
|
|
159
|
-
type: "text",
|
|
160
|
-
text: `
|
|
161
|
-
# Reply Posted Successfully
|
|
162
|
-
|
|
163
|
-
## Comment Details
|
|
164
|
-
- Author: u/${formattedComment.author}
|
|
165
|
-
- Subreddit: r/${formattedComment.context.subreddit}
|
|
166
|
-
- Thread: ${formattedComment.context.thread}
|
|
167
|
-
- Link: ${formattedComment.link}
|
|
168
|
-
|
|
169
|
-
Your reply has been successfully posted.
|
|
170
|
-
`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
// Error will be logged by the server
|
|
176
|
-
throw new McpError(ErrorCode.InternalError, `Failed to reply to post: ${String(error)}`)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
}
|