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
package/dist/bin.js
CHANGED
|
@@ -26,10 +26,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
28
28
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
29
|
-
var
|
|
29
|
+
var import_types6 = require("@modelcontextprotocol/sdk/types.js");
|
|
30
30
|
|
|
31
31
|
// src/client/reddit-client.ts
|
|
32
|
-
var import_axios = __toESM(require("axios"));
|
|
33
32
|
var RedditClient = class {
|
|
34
33
|
clientId;
|
|
35
34
|
clientSecret;
|
|
@@ -38,7 +37,7 @@ var RedditClient = class {
|
|
|
38
37
|
password;
|
|
39
38
|
accessToken;
|
|
40
39
|
tokenExpiry = 0;
|
|
41
|
-
|
|
40
|
+
baseUrl = "https://oauth.reddit.com";
|
|
42
41
|
authenticated = false;
|
|
43
42
|
constructor(config) {
|
|
44
43
|
this.clientId = config.clientId;
|
|
@@ -46,24 +45,33 @@ var RedditClient = class {
|
|
|
46
45
|
this.userAgent = config.userAgent;
|
|
47
46
|
this.username = config.username;
|
|
48
47
|
this.password = config.password;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
}
|
|
49
|
+
async makeRequest(path, options = {}) {
|
|
50
|
+
if (Date.now() >= this.tokenExpiry || !this.authenticated) {
|
|
51
|
+
await this.authenticate();
|
|
52
|
+
}
|
|
53
|
+
const url = `${this.baseUrl}${path}`;
|
|
54
|
+
const headers = {
|
|
55
|
+
"User-Agent": this.userAgent,
|
|
56
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
57
|
+
...options.headers
|
|
58
|
+
};
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
...options,
|
|
61
|
+
headers
|
|
54
62
|
});
|
|
55
|
-
this.
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
if (response.status === 401 && this.authenticated) {
|
|
64
|
+
await this.authenticate();
|
|
65
|
+
const retryHeaders = {
|
|
66
|
+
...headers,
|
|
67
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
68
|
+
};
|
|
69
|
+
return fetch(url, {
|
|
70
|
+
...options,
|
|
71
|
+
headers: retryHeaders
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
67
75
|
}
|
|
68
76
|
async authenticate() {
|
|
69
77
|
try {
|
|
@@ -80,20 +88,23 @@ var RedditClient = class {
|
|
|
80
88
|
} else {
|
|
81
89
|
authData.append("grant_type", "client_credentials");
|
|
82
90
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
password: this.clientSecret
|
|
87
|
-
},
|
|
91
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
|
|
92
|
+
const response = await fetch(authUrl, {
|
|
93
|
+
method: "POST",
|
|
88
94
|
headers: {
|
|
89
95
|
"User-Agent": this.userAgent,
|
|
90
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
91
|
-
|
|
96
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
97
|
+
Authorization: `Basic ${credentials}`
|
|
98
|
+
},
|
|
99
|
+
body: authData.toString()
|
|
92
100
|
});
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Authentication failed: ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
this.accessToken = data.access_token;
|
|
106
|
+
this.tokenExpiry = now + data.expires_in * 1e3;
|
|
95
107
|
this.authenticated = true;
|
|
96
|
-
this.api.defaults.headers.common["Authorization"] = `Bearer ${this.accessToken}`;
|
|
97
108
|
} catch {
|
|
98
109
|
throw new Error("Failed to authenticate with Reddit API");
|
|
99
110
|
}
|
|
@@ -112,8 +123,12 @@ var RedditClient = class {
|
|
|
112
123
|
async getUser(username) {
|
|
113
124
|
await this.authenticate();
|
|
114
125
|
try {
|
|
115
|
-
const response = await this.
|
|
116
|
-
|
|
126
|
+
const response = await this.makeRequest(`/user/${username}/about.json`);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`HTTP ${response.status}`);
|
|
129
|
+
}
|
|
130
|
+
const json = await response.json();
|
|
131
|
+
const data = json.data;
|
|
117
132
|
return {
|
|
118
133
|
name: data.name,
|
|
119
134
|
id: data.id,
|
|
@@ -133,8 +148,12 @@ var RedditClient = class {
|
|
|
133
148
|
async getSubredditInfo(subredditName) {
|
|
134
149
|
await this.authenticate();
|
|
135
150
|
try {
|
|
136
|
-
const response = await this.
|
|
137
|
-
|
|
151
|
+
const response = await this.makeRequest(`/r/${subredditName}/about.json`);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`HTTP ${response.status}`);
|
|
154
|
+
}
|
|
155
|
+
const json = await response.json();
|
|
156
|
+
const data = json.data;
|
|
138
157
|
return {
|
|
139
158
|
displayName: data.display_name,
|
|
140
159
|
title: data.title,
|
|
@@ -155,13 +174,16 @@ var RedditClient = class {
|
|
|
155
174
|
await this.authenticate();
|
|
156
175
|
try {
|
|
157
176
|
const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
limit
|
|
162
|
-
}
|
|
177
|
+
const params = new URLSearchParams({
|
|
178
|
+
t: timeFilter,
|
|
179
|
+
limit: limit.toString()
|
|
163
180
|
});
|
|
164
|
-
|
|
181
|
+
const response = await this.makeRequest(`${endpoint}?${params}`);
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(`HTTP ${response.status}`);
|
|
184
|
+
}
|
|
185
|
+
const json = await response.json();
|
|
186
|
+
return json.data.children.map((child) => {
|
|
165
187
|
const post = child.data;
|
|
166
188
|
return {
|
|
167
189
|
id: post.id,
|
|
@@ -190,15 +212,19 @@ var RedditClient = class {
|
|
|
190
212
|
await this.authenticate();
|
|
191
213
|
try {
|
|
192
214
|
const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
|
|
193
|
-
const response = await this.
|
|
215
|
+
const response = await this.makeRequest(endpoint);
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`HTTP ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
const json = await response.json();
|
|
194
220
|
let post;
|
|
195
221
|
if (subreddit) {
|
|
196
|
-
post =
|
|
222
|
+
post = json[0].data.children[0].data;
|
|
197
223
|
} else {
|
|
198
|
-
if (!
|
|
224
|
+
if (!json.data.children.length) {
|
|
199
225
|
throw new Error(`Post with ID ${postId} not found`);
|
|
200
226
|
}
|
|
201
|
-
post =
|
|
227
|
+
post = json.data.children[0].data;
|
|
202
228
|
}
|
|
203
229
|
return {
|
|
204
230
|
id: post.id,
|
|
@@ -225,10 +251,13 @@ var RedditClient = class {
|
|
|
225
251
|
async getTrendingSubreddits(limit = 5) {
|
|
226
252
|
await this.authenticate();
|
|
227
253
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
const params = new URLSearchParams({ limit: limit.toString() });
|
|
255
|
+
const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`HTTP ${response.status}`);
|
|
258
|
+
}
|
|
259
|
+
const json = await response.json();
|
|
260
|
+
return json.data.children.map((child) => child.data.display_name);
|
|
232
261
|
} catch {
|
|
233
262
|
throw new Error("Failed to get trending subreddits");
|
|
234
263
|
}
|
|
@@ -245,13 +274,19 @@ var RedditClient = class {
|
|
|
245
274
|
params.append("kind", kind);
|
|
246
275
|
params.append("title", title);
|
|
247
276
|
params.append(isSelf ? "text" : "url", content);
|
|
248
|
-
const response = await this.
|
|
277
|
+
const response = await this.makeRequest("/api/submit", {
|
|
278
|
+
method: "POST",
|
|
249
279
|
headers: {
|
|
250
280
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
251
|
-
}
|
|
281
|
+
},
|
|
282
|
+
body: params.toString()
|
|
252
283
|
});
|
|
253
|
-
if (response.
|
|
254
|
-
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
throw new Error(`HTTP ${response.status}`);
|
|
286
|
+
}
|
|
287
|
+
const json = await response.json();
|
|
288
|
+
if (json.success) {
|
|
289
|
+
const postId = json.data.id;
|
|
255
290
|
return await this.getPost(postId);
|
|
256
291
|
} else {
|
|
257
292
|
throw new Error("Failed to create post");
|
|
@@ -263,8 +298,12 @@ var RedditClient = class {
|
|
|
263
298
|
async checkPostExists(postId) {
|
|
264
299
|
await this.authenticate();
|
|
265
300
|
try {
|
|
266
|
-
const response = await this.
|
|
267
|
-
|
|
301
|
+
const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const json = await response.json();
|
|
306
|
+
return json.data.children.length > 0;
|
|
268
307
|
} catch {
|
|
269
308
|
return false;
|
|
270
309
|
}
|
|
@@ -281,12 +320,17 @@ var RedditClient = class {
|
|
|
281
320
|
const params = new URLSearchParams();
|
|
282
321
|
params.append("thing_id", `t3_${postId}`);
|
|
283
322
|
params.append("text", content);
|
|
284
|
-
const response = await this.
|
|
323
|
+
const response = await this.makeRequest("/api/comment", {
|
|
324
|
+
method: "POST",
|
|
285
325
|
headers: {
|
|
286
326
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
287
|
-
}
|
|
327
|
+
},
|
|
328
|
+
body: params.toString()
|
|
288
329
|
});
|
|
289
|
-
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`HTTP ${response.status}`);
|
|
332
|
+
}
|
|
333
|
+
const commentData = await response.json();
|
|
290
334
|
return {
|
|
291
335
|
id: commentData.id,
|
|
292
336
|
author: this.username,
|
|
@@ -304,6 +348,187 @@ var RedditClient = class {
|
|
|
304
348
|
throw new Error(`Failed to reply to post ${postId}`);
|
|
305
349
|
}
|
|
306
350
|
}
|
|
351
|
+
async searchReddit(query, options = {}) {
|
|
352
|
+
await this.authenticate();
|
|
353
|
+
try {
|
|
354
|
+
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
|
|
355
|
+
const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
|
|
356
|
+
const params = new URLSearchParams({
|
|
357
|
+
q: query,
|
|
358
|
+
sort,
|
|
359
|
+
t: timeFilter,
|
|
360
|
+
limit: limit.toString(),
|
|
361
|
+
type,
|
|
362
|
+
...subreddit && { restrict_sr: "true" }
|
|
363
|
+
});
|
|
364
|
+
const response = await this.makeRequest(`${endpoint}?${params}`);
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
throw new Error(`HTTP ${response.status}`);
|
|
367
|
+
}
|
|
368
|
+
const json = await response.json();
|
|
369
|
+
return json.data.children.filter((child) => child.kind === "t3").map((child) => {
|
|
370
|
+
const post = child.data;
|
|
371
|
+
return {
|
|
372
|
+
id: post.id,
|
|
373
|
+
title: post.title,
|
|
374
|
+
author: post.author,
|
|
375
|
+
subreddit: post.subreddit,
|
|
376
|
+
selftext: post.selftext || "",
|
|
377
|
+
url: post.url,
|
|
378
|
+
score: post.score,
|
|
379
|
+
upvoteRatio: post.upvote_ratio,
|
|
380
|
+
numComments: post.num_comments,
|
|
381
|
+
createdUtc: post.created_utc,
|
|
382
|
+
over18: post.over_18,
|
|
383
|
+
spoiler: post.spoiler,
|
|
384
|
+
edited: !!post.edited,
|
|
385
|
+
isSelf: post.is_self,
|
|
386
|
+
linkFlairText: post.link_flair_text,
|
|
387
|
+
permalink: post.permalink
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
} catch {
|
|
391
|
+
throw new Error(`Failed to search Reddit for: ${query}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async getPostComments(postId, subreddit, options = {}) {
|
|
395
|
+
await this.authenticate();
|
|
396
|
+
try {
|
|
397
|
+
const { sort = "best", limit = 100 } = options;
|
|
398
|
+
const params = new URLSearchParams({
|
|
399
|
+
sort,
|
|
400
|
+
limit: limit.toString()
|
|
401
|
+
});
|
|
402
|
+
const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
throw new Error(`HTTP ${response.status}`);
|
|
405
|
+
}
|
|
406
|
+
const json = await response.json();
|
|
407
|
+
const postData = json[0].data.children[0].data;
|
|
408
|
+
const post = {
|
|
409
|
+
id: postData.id,
|
|
410
|
+
title: postData.title,
|
|
411
|
+
author: postData.author,
|
|
412
|
+
subreddit: postData.subreddit,
|
|
413
|
+
selftext: postData.selftext || "",
|
|
414
|
+
url: postData.url,
|
|
415
|
+
score: postData.score,
|
|
416
|
+
upvoteRatio: postData.upvote_ratio,
|
|
417
|
+
numComments: postData.num_comments,
|
|
418
|
+
createdUtc: postData.created_utc,
|
|
419
|
+
over18: postData.over_18,
|
|
420
|
+
spoiler: postData.spoiler,
|
|
421
|
+
edited: !!postData.edited,
|
|
422
|
+
isSelf: postData.is_self,
|
|
423
|
+
linkFlairText: postData.link_flair_text,
|
|
424
|
+
permalink: postData.permalink
|
|
425
|
+
};
|
|
426
|
+
const comments = [];
|
|
427
|
+
const parseComments = (commentData, depth = 0) => {
|
|
428
|
+
for (const item of commentData) {
|
|
429
|
+
if (item.kind === "t1" && item.data.body) {
|
|
430
|
+
comments.push({
|
|
431
|
+
id: item.data.id,
|
|
432
|
+
author: item.data.author,
|
|
433
|
+
body: item.data.body,
|
|
434
|
+
score: item.data.score,
|
|
435
|
+
controversiality: item.data.controversiality,
|
|
436
|
+
subreddit: item.data.subreddit,
|
|
437
|
+
submissionTitle: post.title,
|
|
438
|
+
createdUtc: item.data.created_utc,
|
|
439
|
+
edited: !!item.data.edited,
|
|
440
|
+
isSubmitter: item.data.is_submitter,
|
|
441
|
+
permalink: item.data.permalink,
|
|
442
|
+
depth,
|
|
443
|
+
parentId: item.data.parent_id
|
|
444
|
+
});
|
|
445
|
+
if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
|
|
446
|
+
parseComments(item.data.replies.data.children, depth + 1);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
if (json[1] && json[1].data && json[1].data.children) {
|
|
452
|
+
parseComments(json[1].data.children);
|
|
453
|
+
}
|
|
454
|
+
return { post, comments };
|
|
455
|
+
} catch {
|
|
456
|
+
throw new Error(`Failed to get comments for post ${postId}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async getUserPosts(username, options = {}) {
|
|
460
|
+
await this.authenticate();
|
|
461
|
+
try {
|
|
462
|
+
const { sort = "new", timeFilter = "all", limit = 25 } = options;
|
|
463
|
+
const params = new URLSearchParams({
|
|
464
|
+
sort,
|
|
465
|
+
t: timeFilter,
|
|
466
|
+
limit: limit.toString()
|
|
467
|
+
});
|
|
468
|
+
const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
throw new Error(`HTTP ${response.status}`);
|
|
471
|
+
}
|
|
472
|
+
const json = await response.json();
|
|
473
|
+
return json.data.children.filter((child) => child.kind === "t3").map((child) => {
|
|
474
|
+
const post = child.data;
|
|
475
|
+
return {
|
|
476
|
+
id: post.id,
|
|
477
|
+
title: post.title,
|
|
478
|
+
author: post.author,
|
|
479
|
+
subreddit: post.subreddit,
|
|
480
|
+
selftext: post.selftext || "",
|
|
481
|
+
url: post.url,
|
|
482
|
+
score: post.score,
|
|
483
|
+
upvoteRatio: post.upvote_ratio,
|
|
484
|
+
numComments: post.num_comments,
|
|
485
|
+
createdUtc: post.created_utc,
|
|
486
|
+
over18: post.over_18,
|
|
487
|
+
spoiler: post.spoiler,
|
|
488
|
+
edited: !!post.edited,
|
|
489
|
+
isSelf: post.is_self,
|
|
490
|
+
linkFlairText: post.link_flair_text,
|
|
491
|
+
permalink: post.permalink
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
} catch {
|
|
495
|
+
throw new Error(`Failed to get posts for user ${username}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async getUserComments(username, options = {}) {
|
|
499
|
+
await this.authenticate();
|
|
500
|
+
try {
|
|
501
|
+
const { sort = "new", timeFilter = "all", limit = 25 } = options;
|
|
502
|
+
const params = new URLSearchParams({
|
|
503
|
+
sort,
|
|
504
|
+
t: timeFilter,
|
|
505
|
+
limit: limit.toString()
|
|
506
|
+
});
|
|
507
|
+
const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
throw new Error(`HTTP ${response.status}`);
|
|
510
|
+
}
|
|
511
|
+
const json = await response.json();
|
|
512
|
+
return json.data.children.filter((child) => child.kind === "t1").map((child) => {
|
|
513
|
+
const comment = child.data;
|
|
514
|
+
return {
|
|
515
|
+
id: comment.id,
|
|
516
|
+
author: comment.author,
|
|
517
|
+
body: comment.body,
|
|
518
|
+
score: comment.score,
|
|
519
|
+
controversiality: comment.controversiality,
|
|
520
|
+
subreddit: comment.subreddit,
|
|
521
|
+
submissionTitle: comment.link_title || "",
|
|
522
|
+
createdUtc: comment.created_utc,
|
|
523
|
+
edited: !!comment.edited,
|
|
524
|
+
isSubmitter: comment.is_submitter,
|
|
525
|
+
permalink: comment.permalink
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
} catch {
|
|
529
|
+
throw new Error(`Failed to get comments for user ${username}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
307
532
|
};
|
|
308
533
|
var redditClient = null;
|
|
309
534
|
function initializeRedditClient(config) {
|
|
@@ -548,6 +773,21 @@ function formatCommentInfo(comment) {
|
|
|
548
773
|
commentAnalysis: analyzeCommentImpact(comment.score, comment.edited, comment.isSubmitter)
|
|
549
774
|
};
|
|
550
775
|
}
|
|
776
|
+
function formatPost(post) {
|
|
777
|
+
return {
|
|
778
|
+
title: post.title,
|
|
779
|
+
author: post.author,
|
|
780
|
+
subreddit: post.subreddit,
|
|
781
|
+
score: post.score,
|
|
782
|
+
upvoteRatio: Math.round(post.upvoteRatio * 100),
|
|
783
|
+
numComments: post.numComments,
|
|
784
|
+
createdAt: formatTimestamp(post.createdUtc),
|
|
785
|
+
selftext: post.selftext,
|
|
786
|
+
permalink: post.permalink,
|
|
787
|
+
nsfw: post.over18,
|
|
788
|
+
spoiler: post.spoiler
|
|
789
|
+
};
|
|
790
|
+
}
|
|
551
791
|
|
|
552
792
|
// src/tools/user-tools.ts
|
|
553
793
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -590,6 +830,86 @@ async function getUserInfo(params) {
|
|
|
590
830
|
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user data: ${String(error)}`);
|
|
591
831
|
}
|
|
592
832
|
}
|
|
833
|
+
async function getUserPosts(params) {
|
|
834
|
+
const { username, sort = "new", time_filter = "all", limit = 10 } = params;
|
|
835
|
+
const client = getRedditClient();
|
|
836
|
+
if (!client) {
|
|
837
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
const posts = await client.getUserPosts(username, {
|
|
841
|
+
sort,
|
|
842
|
+
timeFilter: time_filter,
|
|
843
|
+
limit
|
|
844
|
+
});
|
|
845
|
+
return {
|
|
846
|
+
content: [
|
|
847
|
+
{
|
|
848
|
+
type: "text",
|
|
849
|
+
text: `# Posts by u/${username}
|
|
850
|
+
|
|
851
|
+
## Sort: ${sort} | Time: ${time_filter} | Count: ${posts.length}
|
|
852
|
+
|
|
853
|
+
${posts.map((post, index) => {
|
|
854
|
+
const date = new Date(post.createdUtc * 1e3).toLocaleString();
|
|
855
|
+
const selftext = post.selftext ? `
|
|
856
|
+
${post.selftext.substring(0, 200)}${post.selftext.length > 200 ? "..." : ""}
|
|
857
|
+
` : "";
|
|
858
|
+
return `### ${index + 1}. ${post.title}
|
|
859
|
+
- Subreddit: r/${post.subreddit}
|
|
860
|
+
- Score: ${post.score} (${Math.round(post.upvoteRatio * 100)}% upvoted)
|
|
861
|
+
- Comments: ${post.numComments}
|
|
862
|
+
- Posted: ${date}
|
|
863
|
+
${selftext}
|
|
864
|
+
- Link: https://reddit.com${post.permalink}
|
|
865
|
+
${post.over18 ? "- **NSFW**" : ""}
|
|
866
|
+
${post.spoiler ? "- **Spoiler**" : ""}`;
|
|
867
|
+
}).join("\n\n---\n\n")}`
|
|
868
|
+
}
|
|
869
|
+
]
|
|
870
|
+
};
|
|
871
|
+
} catch (error) {
|
|
872
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user posts: ${String(error)}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function getUserComments(params) {
|
|
876
|
+
const { username, sort = "new", time_filter = "all", limit = 10 } = params;
|
|
877
|
+
const client = getRedditClient();
|
|
878
|
+
if (!client) {
|
|
879
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const comments = await client.getUserComments(username, {
|
|
883
|
+
sort,
|
|
884
|
+
timeFilter: time_filter,
|
|
885
|
+
limit
|
|
886
|
+
});
|
|
887
|
+
return {
|
|
888
|
+
content: [
|
|
889
|
+
{
|
|
890
|
+
type: "text",
|
|
891
|
+
text: `# Comments by u/${username}
|
|
892
|
+
|
|
893
|
+
## Sort: ${sort} | Time: ${time_filter} | Count: ${comments.length}
|
|
894
|
+
|
|
895
|
+
${comments.map((comment, index) => {
|
|
896
|
+
const date = new Date(comment.createdUtc * 1e3).toLocaleString();
|
|
897
|
+
const edited = comment.edited ? " *(edited)*" : "";
|
|
898
|
+
const body = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body;
|
|
899
|
+
return `### ${index + 1}. In r/${comment.subreddit} on "${comment.submissionTitle}"
|
|
900
|
+
- Score: ${comment.score} points
|
|
901
|
+
- Posted: ${date}${edited}
|
|
902
|
+
- Link: https://reddit.com${comment.permalink}
|
|
903
|
+
|
|
904
|
+
${body}`;
|
|
905
|
+
}).join("\n\n---\n\n")}`
|
|
906
|
+
}
|
|
907
|
+
]
|
|
908
|
+
};
|
|
909
|
+
} catch (error) {
|
|
910
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user comments: ${String(error)}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
593
913
|
|
|
594
914
|
// src/tools/post-tools.ts
|
|
595
915
|
var import_types2 = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -817,6 +1137,114 @@ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).j
|
|
|
817
1137
|
}
|
|
818
1138
|
}
|
|
819
1139
|
|
|
1140
|
+
// src/tools/search-tools.ts
|
|
1141
|
+
var import_types4 = require("@modelcontextprotocol/sdk/types.js");
|
|
1142
|
+
async function searchReddit(params) {
|
|
1143
|
+
const { query, subreddit, sort = "relevance", time_filter = "all", limit = 10, type = "link" } = params;
|
|
1144
|
+
const client = getRedditClient();
|
|
1145
|
+
if (!client) {
|
|
1146
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InternalError, "Reddit client not initialized");
|
|
1147
|
+
}
|
|
1148
|
+
if (!query || query.trim().length === 0) {
|
|
1149
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, "Search query cannot be empty");
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
const posts = await client.searchReddit(query, {
|
|
1153
|
+
subreddit,
|
|
1154
|
+
sort,
|
|
1155
|
+
timeFilter: time_filter,
|
|
1156
|
+
limit,
|
|
1157
|
+
type
|
|
1158
|
+
});
|
|
1159
|
+
return {
|
|
1160
|
+
content: [
|
|
1161
|
+
{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: `# Reddit Search Results for: "${query}"${subreddit ? ` in r/${subreddit}` : ""}
|
|
1164
|
+
|
|
1165
|
+
## Search Parameters
|
|
1166
|
+
- Sort: ${sort}
|
|
1167
|
+
- Time Filter: ${time_filter}
|
|
1168
|
+
- Type: ${type}
|
|
1169
|
+
- Results: ${posts.length}
|
|
1170
|
+
|
|
1171
|
+
${posts.map((post, index) => {
|
|
1172
|
+
const formatted = formatPost(post);
|
|
1173
|
+
return `### ${index + 1}. ${formatted.title}
|
|
1174
|
+
- Author: u/${formatted.author}
|
|
1175
|
+
- Subreddit: r/${formatted.subreddit}
|
|
1176
|
+
- Score: ${formatted.score} (${formatted.upvoteRatio}% upvoted)
|
|
1177
|
+
- Comments: ${formatted.numComments}
|
|
1178
|
+
- Posted: ${formatted.createdAt}
|
|
1179
|
+
${formatted.selftext ? `
|
|
1180
|
+
${formatted.selftext.substring(0, 200)}${formatted.selftext.length > 200 ? "..." : ""}
|
|
1181
|
+
` : ""}
|
|
1182
|
+
- Link: https://reddit.com${formatted.permalink}
|
|
1183
|
+
${formatted.nsfw ? "- **NSFW**" : ""}
|
|
1184
|
+
${formatted.spoiler ? "- **Spoiler**" : ""}
|
|
1185
|
+
`;
|
|
1186
|
+
}).join("\n")}`
|
|
1187
|
+
}
|
|
1188
|
+
]
|
|
1189
|
+
};
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InternalError, `Failed to search Reddit: ${String(error)}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/tools/comment-tools.ts
|
|
1196
|
+
var import_types5 = require("@modelcontextprotocol/sdk/types.js");
|
|
1197
|
+
async function getPostComments(params) {
|
|
1198
|
+
const { post_id, subreddit, sort = "best", limit = 100 } = params;
|
|
1199
|
+
const client = getRedditClient();
|
|
1200
|
+
if (!client) {
|
|
1201
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InternalError, "Reddit client not initialized");
|
|
1202
|
+
}
|
|
1203
|
+
if (!post_id || !subreddit) {
|
|
1204
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InvalidParams, "post_id and subreddit are required");
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const { post, comments } = await client.getPostComments(post_id, subreddit, {
|
|
1208
|
+
sort,
|
|
1209
|
+
limit
|
|
1210
|
+
});
|
|
1211
|
+
const formattedPost = formatPost(post);
|
|
1212
|
+
const formatComment = (comment) => {
|
|
1213
|
+
const edited = comment.edited ? " *(edited)*" : "";
|
|
1214
|
+
const submitter = comment.isSubmitter ? " **[OP]**" : "";
|
|
1215
|
+
const depth = comment.depth || 0;
|
|
1216
|
+
const prefix = " ".repeat(depth) + (depth > 0 ? "\u2514\u2500 " : "");
|
|
1217
|
+
return `${prefix}**u/${comment.author}**${submitter} \u2022 ${comment.score} points \u2022 ${new Date(comment.createdUtc * 1e3).toLocaleString()}${edited}
|
|
1218
|
+
${prefix}${comment.body.split("\n").join(`
|
|
1219
|
+
${prefix}`)}`;
|
|
1220
|
+
};
|
|
1221
|
+
return {
|
|
1222
|
+
content: [
|
|
1223
|
+
{
|
|
1224
|
+
type: "text",
|
|
1225
|
+
text: `# Comments for: ${formattedPost.title}
|
|
1226
|
+
|
|
1227
|
+
## Post Details
|
|
1228
|
+
- Author: u/${formattedPost.author}
|
|
1229
|
+
- Subreddit: r/${formattedPost.subreddit}
|
|
1230
|
+
- Score: ${formattedPost.score} (${formattedPost.upvoteRatio}% upvoted)
|
|
1231
|
+
- Posted: ${formattedPost.createdAt}
|
|
1232
|
+
- Link: https://reddit.com${formattedPost.permalink}
|
|
1233
|
+
|
|
1234
|
+
## Post Content
|
|
1235
|
+
${formattedPost.selftext || "[Link post - no text content]"}
|
|
1236
|
+
|
|
1237
|
+
## Comments (${comments.length} loaded, sorted by ${sort})
|
|
1238
|
+
|
|
1239
|
+
${comments.map((comment) => formatComment(comment)).join("\n\n---\n\n")}`
|
|
1240
|
+
}
|
|
1241
|
+
]
|
|
1242
|
+
};
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InternalError, `Failed to fetch comments: ${String(error)}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
820
1248
|
// src/index.ts
|
|
821
1249
|
var import_dotenv = __toESM(require("dotenv"));
|
|
822
1250
|
import_dotenv.default.config();
|
|
@@ -855,9 +1283,6 @@ var RedditServer = class {
|
|
|
855
1283
|
const username = process.env.REDDIT_USERNAME;
|
|
856
1284
|
const password = process.env.REDDIT_PASSWORD;
|
|
857
1285
|
if (!clientId || !clientSecret) {
|
|
858
|
-
console.error(
|
|
859
|
-
"[Error] Missing required Reddit API credentials. Please set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables."
|
|
860
|
-
);
|
|
861
1286
|
process.exit(1);
|
|
862
1287
|
}
|
|
863
1288
|
try {
|
|
@@ -868,13 +1293,12 @@ var RedditServer = class {
|
|
|
868
1293
|
username,
|
|
869
1294
|
password
|
|
870
1295
|
});
|
|
871
|
-
} catch
|
|
872
|
-
console.error("[Error] Failed to initialize Reddit client:", error);
|
|
1296
|
+
} catch {
|
|
873
1297
|
process.exit(1);
|
|
874
1298
|
}
|
|
875
1299
|
}
|
|
876
1300
|
setupToolHandlers() {
|
|
877
|
-
this.server.setRequestHandler(
|
|
1301
|
+
this.server.setRequestHandler(import_types6.ListToolsRequestSchema, async () => ({
|
|
878
1302
|
tools: [
|
|
879
1303
|
{
|
|
880
1304
|
name: "test_reddit_mcp_server",
|
|
@@ -1013,10 +1437,150 @@ var RedditServer = class {
|
|
|
1013
1437
|
},
|
|
1014
1438
|
required: ["post_id", "content"]
|
|
1015
1439
|
}
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
name: "search_reddit",
|
|
1443
|
+
description: "Search for posts on Reddit",
|
|
1444
|
+
inputSchema: {
|
|
1445
|
+
type: "object",
|
|
1446
|
+
properties: {
|
|
1447
|
+
query: {
|
|
1448
|
+
type: "string",
|
|
1449
|
+
description: "The search query"
|
|
1450
|
+
},
|
|
1451
|
+
subreddit: {
|
|
1452
|
+
type: "string",
|
|
1453
|
+
description: "Search within a specific subreddit (optional)"
|
|
1454
|
+
},
|
|
1455
|
+
sort: {
|
|
1456
|
+
type: "string",
|
|
1457
|
+
description: "Sort order: relevance, hot, top, new, comments",
|
|
1458
|
+
enum: ["relevance", "hot", "top", "new", "comments"],
|
|
1459
|
+
default: "relevance"
|
|
1460
|
+
},
|
|
1461
|
+
time_filter: {
|
|
1462
|
+
type: "string",
|
|
1463
|
+
description: "Time filter: hour, day, week, month, year, all",
|
|
1464
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1465
|
+
default: "all"
|
|
1466
|
+
},
|
|
1467
|
+
limit: {
|
|
1468
|
+
type: "number",
|
|
1469
|
+
description: "Maximum number of results to return",
|
|
1470
|
+
minimum: 1,
|
|
1471
|
+
maximum: 100,
|
|
1472
|
+
default: 10
|
|
1473
|
+
},
|
|
1474
|
+
type: {
|
|
1475
|
+
type: "string",
|
|
1476
|
+
description: "Type of content: link (posts), sr (subreddits), user (users)",
|
|
1477
|
+
enum: ["link", "sr", "user"],
|
|
1478
|
+
default: "link"
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
required: ["query"]
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
name: "get_post_comments",
|
|
1486
|
+
description: "Get comments for a specific Reddit post",
|
|
1487
|
+
inputSchema: {
|
|
1488
|
+
type: "object",
|
|
1489
|
+
properties: {
|
|
1490
|
+
post_id: {
|
|
1491
|
+
type: "string",
|
|
1492
|
+
description: "The ID of the post"
|
|
1493
|
+
},
|
|
1494
|
+
subreddit: {
|
|
1495
|
+
type: "string",
|
|
1496
|
+
description: "The subreddit where the post is located"
|
|
1497
|
+
},
|
|
1498
|
+
sort: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Comment sort order: best, top, new, controversial, old, qa",
|
|
1501
|
+
enum: ["best", "top", "new", "controversial", "old", "qa"],
|
|
1502
|
+
default: "best"
|
|
1503
|
+
},
|
|
1504
|
+
limit: {
|
|
1505
|
+
type: "number",
|
|
1506
|
+
description: "Maximum number of comments to load",
|
|
1507
|
+
minimum: 1,
|
|
1508
|
+
maximum: 500,
|
|
1509
|
+
default: 100
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
required: ["post_id", "subreddit"]
|
|
1513
|
+
}
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
name: "get_user_posts",
|
|
1517
|
+
description: "Get posts submitted by a specific user",
|
|
1518
|
+
inputSchema: {
|
|
1519
|
+
type: "object",
|
|
1520
|
+
properties: {
|
|
1521
|
+
username: {
|
|
1522
|
+
type: "string",
|
|
1523
|
+
description: "The username to get posts for"
|
|
1524
|
+
},
|
|
1525
|
+
sort: {
|
|
1526
|
+
type: "string",
|
|
1527
|
+
description: "Sort order: new, hot, top, controversial",
|
|
1528
|
+
enum: ["new", "hot", "top", "controversial"],
|
|
1529
|
+
default: "new"
|
|
1530
|
+
},
|
|
1531
|
+
time_filter: {
|
|
1532
|
+
type: "string",
|
|
1533
|
+
description: "Time filter for top/controversial: hour, day, week, month, year, all",
|
|
1534
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1535
|
+
default: "all"
|
|
1536
|
+
},
|
|
1537
|
+
limit: {
|
|
1538
|
+
type: "number",
|
|
1539
|
+
description: "Maximum number of posts to return",
|
|
1540
|
+
minimum: 1,
|
|
1541
|
+
maximum: 100,
|
|
1542
|
+
default: 10
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
required: ["username"]
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
name: "get_user_comments",
|
|
1550
|
+
description: "Get comments made by a specific user",
|
|
1551
|
+
inputSchema: {
|
|
1552
|
+
type: "object",
|
|
1553
|
+
properties: {
|
|
1554
|
+
username: {
|
|
1555
|
+
type: "string",
|
|
1556
|
+
description: "The username to get comments for"
|
|
1557
|
+
},
|
|
1558
|
+
sort: {
|
|
1559
|
+
type: "string",
|
|
1560
|
+
description: "Sort order: new, hot, top, controversial",
|
|
1561
|
+
enum: ["new", "hot", "top", "controversial"],
|
|
1562
|
+
default: "new"
|
|
1563
|
+
},
|
|
1564
|
+
time_filter: {
|
|
1565
|
+
type: "string",
|
|
1566
|
+
description: "Time filter for top/controversial: hour, day, week, month, year, all",
|
|
1567
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1568
|
+
default: "all"
|
|
1569
|
+
},
|
|
1570
|
+
limit: {
|
|
1571
|
+
type: "number",
|
|
1572
|
+
description: "Maximum number of comments to return",
|
|
1573
|
+
minimum: 1,
|
|
1574
|
+
maximum: 100,
|
|
1575
|
+
default: 10
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
required: ["username"]
|
|
1579
|
+
}
|
|
1016
1580
|
}
|
|
1017
1581
|
]
|
|
1018
1582
|
}));
|
|
1019
|
-
this.server.setRequestHandler(
|
|
1583
|
+
this.server.setRequestHandler(import_types6.CallToolRequestSchema, async (request) => {
|
|
1020
1584
|
try {
|
|
1021
1585
|
const toolName = request.params.name;
|
|
1022
1586
|
const toolParams = request.params.arguments || {};
|
|
@@ -1055,8 +1619,24 @@ var RedditServer = class {
|
|
|
1055
1619
|
return await replyToPost(
|
|
1056
1620
|
toolParams
|
|
1057
1621
|
);
|
|
1622
|
+
case "search_reddit":
|
|
1623
|
+
return await searchReddit(
|
|
1624
|
+
toolParams
|
|
1625
|
+
);
|
|
1626
|
+
case "get_post_comments":
|
|
1627
|
+
return await getPostComments(
|
|
1628
|
+
toolParams
|
|
1629
|
+
);
|
|
1630
|
+
case "get_user_posts":
|
|
1631
|
+
return await getUserPosts(
|
|
1632
|
+
toolParams
|
|
1633
|
+
);
|
|
1634
|
+
case "get_user_comments":
|
|
1635
|
+
return await getUserComments(
|
|
1636
|
+
toolParams
|
|
1637
|
+
);
|
|
1058
1638
|
default:
|
|
1059
|
-
throw new
|
|
1639
|
+
throw new import_types6.McpError(import_types6.ErrorCode.MethodNotFound, `Tool with name ${toolName} not found`);
|
|
1060
1640
|
}
|
|
1061
1641
|
} catch (error) {
|
|
1062
1642
|
if (error instanceof Error) {
|
|
@@ -1065,7 +1645,7 @@ var RedditServer = class {
|
|
|
1065
1645
|
logger: "reddit-server",
|
|
1066
1646
|
data: `Error calling tool: ${error.message}`
|
|
1067
1647
|
});
|
|
1068
|
-
throw new
|
|
1648
|
+
throw new import_types6.McpError(import_types6.ErrorCode.InternalError, `Failed to fetch data: ${error.message}`);
|
|
1069
1649
|
}
|
|
1070
1650
|
throw error;
|
|
1071
1651
|
}
|
|
@@ -1090,9 +1670,13 @@ var RedditServer = class {
|
|
|
1090
1670
|
};
|
|
1091
1671
|
if (require.main === module) {
|
|
1092
1672
|
const server2 = new RedditServer();
|
|
1093
|
-
server2.run().catch(
|
|
1673
|
+
server2.run().catch(() => {
|
|
1674
|
+
process.exit(1);
|
|
1675
|
+
});
|
|
1094
1676
|
}
|
|
1095
1677
|
|
|
1096
1678
|
// src/bin.ts
|
|
1097
1679
|
var server = new RedditServer();
|
|
1098
|
-
server.run().catch(
|
|
1680
|
+
server.run().catch(() => {
|
|
1681
|
+
process.exit(1);
|
|
1682
|
+
});
|