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/index.js
CHANGED
|
@@ -35,10 +35,9 @@ __export(src_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(src_exports);
|
|
36
36
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
37
37
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
38
|
-
var
|
|
38
|
+
var import_types6 = require("@modelcontextprotocol/sdk/types.js");
|
|
39
39
|
|
|
40
40
|
// src/client/reddit-client.ts
|
|
41
|
-
var import_axios = __toESM(require("axios"));
|
|
42
41
|
var RedditClient = class {
|
|
43
42
|
clientId;
|
|
44
43
|
clientSecret;
|
|
@@ -47,7 +46,7 @@ var RedditClient = class {
|
|
|
47
46
|
password;
|
|
48
47
|
accessToken;
|
|
49
48
|
tokenExpiry = 0;
|
|
50
|
-
|
|
49
|
+
baseUrl = "https://oauth.reddit.com";
|
|
51
50
|
authenticated = false;
|
|
52
51
|
constructor(config) {
|
|
53
52
|
this.clientId = config.clientId;
|
|
@@ -55,24 +54,33 @@ var RedditClient = class {
|
|
|
55
54
|
this.userAgent = config.userAgent;
|
|
56
55
|
this.username = config.username;
|
|
57
56
|
this.password = config.password;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
}
|
|
58
|
+
async makeRequest(path, options = {}) {
|
|
59
|
+
if (Date.now() >= this.tokenExpiry || !this.authenticated) {
|
|
60
|
+
await this.authenticate();
|
|
61
|
+
}
|
|
62
|
+
const url = `${this.baseUrl}${path}`;
|
|
63
|
+
const headers = {
|
|
64
|
+
"User-Agent": this.userAgent,
|
|
65
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
66
|
+
...options.headers
|
|
67
|
+
};
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
...options,
|
|
70
|
+
headers
|
|
63
71
|
});
|
|
64
|
-
this.
|
|
65
|
-
(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
if (response.status === 401 && this.authenticated) {
|
|
73
|
+
await this.authenticate();
|
|
74
|
+
const retryHeaders = {
|
|
75
|
+
...headers,
|
|
76
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
77
|
+
};
|
|
78
|
+
return fetch(url, {
|
|
79
|
+
...options,
|
|
80
|
+
headers: retryHeaders
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return response;
|
|
76
84
|
}
|
|
77
85
|
async authenticate() {
|
|
78
86
|
try {
|
|
@@ -89,20 +97,23 @@ var RedditClient = class {
|
|
|
89
97
|
} else {
|
|
90
98
|
authData.append("grant_type", "client_credentials");
|
|
91
99
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
password: this.clientSecret
|
|
96
|
-
},
|
|
100
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
|
|
101
|
+
const response = await fetch(authUrl, {
|
|
102
|
+
method: "POST",
|
|
97
103
|
headers: {
|
|
98
104
|
"User-Agent": this.userAgent,
|
|
99
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
100
|
-
|
|
105
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
106
|
+
Authorization: `Basic ${credentials}`
|
|
107
|
+
},
|
|
108
|
+
body: authData.toString()
|
|
101
109
|
});
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`Authentication failed: ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
this.accessToken = data.access_token;
|
|
115
|
+
this.tokenExpiry = now + data.expires_in * 1e3;
|
|
104
116
|
this.authenticated = true;
|
|
105
|
-
this.api.defaults.headers.common["Authorization"] = `Bearer ${this.accessToken}`;
|
|
106
117
|
} catch {
|
|
107
118
|
throw new Error("Failed to authenticate with Reddit API");
|
|
108
119
|
}
|
|
@@ -121,8 +132,12 @@ var RedditClient = class {
|
|
|
121
132
|
async getUser(username) {
|
|
122
133
|
await this.authenticate();
|
|
123
134
|
try {
|
|
124
|
-
const response = await this.
|
|
125
|
-
|
|
135
|
+
const response = await this.makeRequest(`/user/${username}/about.json`);
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`HTTP ${response.status}`);
|
|
138
|
+
}
|
|
139
|
+
const json = await response.json();
|
|
140
|
+
const data = json.data;
|
|
126
141
|
return {
|
|
127
142
|
name: data.name,
|
|
128
143
|
id: data.id,
|
|
@@ -142,8 +157,12 @@ var RedditClient = class {
|
|
|
142
157
|
async getSubredditInfo(subredditName) {
|
|
143
158
|
await this.authenticate();
|
|
144
159
|
try {
|
|
145
|
-
const response = await this.
|
|
146
|
-
|
|
160
|
+
const response = await this.makeRequest(`/r/${subredditName}/about.json`);
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`HTTP ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
const json = await response.json();
|
|
165
|
+
const data = json.data;
|
|
147
166
|
return {
|
|
148
167
|
displayName: data.display_name,
|
|
149
168
|
title: data.title,
|
|
@@ -164,13 +183,16 @@ var RedditClient = class {
|
|
|
164
183
|
await this.authenticate();
|
|
165
184
|
try {
|
|
166
185
|
const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
limit
|
|
171
|
-
}
|
|
186
|
+
const params = new URLSearchParams({
|
|
187
|
+
t: timeFilter,
|
|
188
|
+
limit: limit.toString()
|
|
172
189
|
});
|
|
173
|
-
|
|
190
|
+
const response = await this.makeRequest(`${endpoint}?${params}`);
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(`HTTP ${response.status}`);
|
|
193
|
+
}
|
|
194
|
+
const json = await response.json();
|
|
195
|
+
return json.data.children.map((child) => {
|
|
174
196
|
const post = child.data;
|
|
175
197
|
return {
|
|
176
198
|
id: post.id,
|
|
@@ -199,15 +221,19 @@ var RedditClient = class {
|
|
|
199
221
|
await this.authenticate();
|
|
200
222
|
try {
|
|
201
223
|
const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
|
|
202
|
-
const response = await this.
|
|
224
|
+
const response = await this.makeRequest(endpoint);
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`HTTP ${response.status}`);
|
|
227
|
+
}
|
|
228
|
+
const json = await response.json();
|
|
203
229
|
let post;
|
|
204
230
|
if (subreddit) {
|
|
205
|
-
post =
|
|
231
|
+
post = json[0].data.children[0].data;
|
|
206
232
|
} else {
|
|
207
|
-
if (!
|
|
233
|
+
if (!json.data.children.length) {
|
|
208
234
|
throw new Error(`Post with ID ${postId} not found`);
|
|
209
235
|
}
|
|
210
|
-
post =
|
|
236
|
+
post = json.data.children[0].data;
|
|
211
237
|
}
|
|
212
238
|
return {
|
|
213
239
|
id: post.id,
|
|
@@ -234,10 +260,13 @@ var RedditClient = class {
|
|
|
234
260
|
async getTrendingSubreddits(limit = 5) {
|
|
235
261
|
await this.authenticate();
|
|
236
262
|
try {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
const params = new URLSearchParams({ limit: limit.toString() });
|
|
264
|
+
const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
throw new Error(`HTTP ${response.status}`);
|
|
267
|
+
}
|
|
268
|
+
const json = await response.json();
|
|
269
|
+
return json.data.children.map((child) => child.data.display_name);
|
|
241
270
|
} catch {
|
|
242
271
|
throw new Error("Failed to get trending subreddits");
|
|
243
272
|
}
|
|
@@ -254,13 +283,19 @@ var RedditClient = class {
|
|
|
254
283
|
params.append("kind", kind);
|
|
255
284
|
params.append("title", title);
|
|
256
285
|
params.append(isSelf ? "text" : "url", content);
|
|
257
|
-
const response = await this.
|
|
286
|
+
const response = await this.makeRequest("/api/submit", {
|
|
287
|
+
method: "POST",
|
|
258
288
|
headers: {
|
|
259
289
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
260
|
-
}
|
|
290
|
+
},
|
|
291
|
+
body: params.toString()
|
|
261
292
|
});
|
|
262
|
-
if (response.
|
|
263
|
-
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`HTTP ${response.status}`);
|
|
295
|
+
}
|
|
296
|
+
const json = await response.json();
|
|
297
|
+
if (json.success) {
|
|
298
|
+
const postId = json.data.id;
|
|
264
299
|
return await this.getPost(postId);
|
|
265
300
|
} else {
|
|
266
301
|
throw new Error("Failed to create post");
|
|
@@ -272,8 +307,12 @@ var RedditClient = class {
|
|
|
272
307
|
async checkPostExists(postId) {
|
|
273
308
|
await this.authenticate();
|
|
274
309
|
try {
|
|
275
|
-
const response = await this.
|
|
276
|
-
|
|
310
|
+
const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
|
|
311
|
+
if (!response.ok) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const json = await response.json();
|
|
315
|
+
return json.data.children.length > 0;
|
|
277
316
|
} catch {
|
|
278
317
|
return false;
|
|
279
318
|
}
|
|
@@ -290,12 +329,17 @@ var RedditClient = class {
|
|
|
290
329
|
const params = new URLSearchParams();
|
|
291
330
|
params.append("thing_id", `t3_${postId}`);
|
|
292
331
|
params.append("text", content);
|
|
293
|
-
const response = await this.
|
|
332
|
+
const response = await this.makeRequest("/api/comment", {
|
|
333
|
+
method: "POST",
|
|
294
334
|
headers: {
|
|
295
335
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
296
|
-
}
|
|
336
|
+
},
|
|
337
|
+
body: params.toString()
|
|
297
338
|
});
|
|
298
|
-
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
throw new Error(`HTTP ${response.status}`);
|
|
341
|
+
}
|
|
342
|
+
const commentData = await response.json();
|
|
299
343
|
return {
|
|
300
344
|
id: commentData.id,
|
|
301
345
|
author: this.username,
|
|
@@ -313,6 +357,187 @@ var RedditClient = class {
|
|
|
313
357
|
throw new Error(`Failed to reply to post ${postId}`);
|
|
314
358
|
}
|
|
315
359
|
}
|
|
360
|
+
async searchReddit(query, options = {}) {
|
|
361
|
+
await this.authenticate();
|
|
362
|
+
try {
|
|
363
|
+
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
|
|
364
|
+
const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
|
|
365
|
+
const params = new URLSearchParams({
|
|
366
|
+
q: query,
|
|
367
|
+
sort,
|
|
368
|
+
t: timeFilter,
|
|
369
|
+
limit: limit.toString(),
|
|
370
|
+
type,
|
|
371
|
+
...subreddit && { restrict_sr: "true" }
|
|
372
|
+
});
|
|
373
|
+
const response = await this.makeRequest(`${endpoint}?${params}`);
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`HTTP ${response.status}`);
|
|
376
|
+
}
|
|
377
|
+
const json = await response.json();
|
|
378
|
+
return json.data.children.filter((child) => child.kind === "t3").map((child) => {
|
|
379
|
+
const post = child.data;
|
|
380
|
+
return {
|
|
381
|
+
id: post.id,
|
|
382
|
+
title: post.title,
|
|
383
|
+
author: post.author,
|
|
384
|
+
subreddit: post.subreddit,
|
|
385
|
+
selftext: post.selftext || "",
|
|
386
|
+
url: post.url,
|
|
387
|
+
score: post.score,
|
|
388
|
+
upvoteRatio: post.upvote_ratio,
|
|
389
|
+
numComments: post.num_comments,
|
|
390
|
+
createdUtc: post.created_utc,
|
|
391
|
+
over18: post.over_18,
|
|
392
|
+
spoiler: post.spoiler,
|
|
393
|
+
edited: !!post.edited,
|
|
394
|
+
isSelf: post.is_self,
|
|
395
|
+
linkFlairText: post.link_flair_text,
|
|
396
|
+
permalink: post.permalink
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
throw new Error(`Failed to search Reddit for: ${query}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async getPostComments(postId, subreddit, options = {}) {
|
|
404
|
+
await this.authenticate();
|
|
405
|
+
try {
|
|
406
|
+
const { sort = "best", limit = 100 } = options;
|
|
407
|
+
const params = new URLSearchParams({
|
|
408
|
+
sort,
|
|
409
|
+
limit: limit.toString()
|
|
410
|
+
});
|
|
411
|
+
const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
throw new Error(`HTTP ${response.status}`);
|
|
414
|
+
}
|
|
415
|
+
const json = await response.json();
|
|
416
|
+
const postData = json[0].data.children[0].data;
|
|
417
|
+
const post = {
|
|
418
|
+
id: postData.id,
|
|
419
|
+
title: postData.title,
|
|
420
|
+
author: postData.author,
|
|
421
|
+
subreddit: postData.subreddit,
|
|
422
|
+
selftext: postData.selftext || "",
|
|
423
|
+
url: postData.url,
|
|
424
|
+
score: postData.score,
|
|
425
|
+
upvoteRatio: postData.upvote_ratio,
|
|
426
|
+
numComments: postData.num_comments,
|
|
427
|
+
createdUtc: postData.created_utc,
|
|
428
|
+
over18: postData.over_18,
|
|
429
|
+
spoiler: postData.spoiler,
|
|
430
|
+
edited: !!postData.edited,
|
|
431
|
+
isSelf: postData.is_self,
|
|
432
|
+
linkFlairText: postData.link_flair_text,
|
|
433
|
+
permalink: postData.permalink
|
|
434
|
+
};
|
|
435
|
+
const comments = [];
|
|
436
|
+
const parseComments = (commentData, depth = 0) => {
|
|
437
|
+
for (const item of commentData) {
|
|
438
|
+
if (item.kind === "t1" && item.data.body) {
|
|
439
|
+
comments.push({
|
|
440
|
+
id: item.data.id,
|
|
441
|
+
author: item.data.author,
|
|
442
|
+
body: item.data.body,
|
|
443
|
+
score: item.data.score,
|
|
444
|
+
controversiality: item.data.controversiality,
|
|
445
|
+
subreddit: item.data.subreddit,
|
|
446
|
+
submissionTitle: post.title,
|
|
447
|
+
createdUtc: item.data.created_utc,
|
|
448
|
+
edited: !!item.data.edited,
|
|
449
|
+
isSubmitter: item.data.is_submitter,
|
|
450
|
+
permalink: item.data.permalink,
|
|
451
|
+
depth,
|
|
452
|
+
parentId: item.data.parent_id
|
|
453
|
+
});
|
|
454
|
+
if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
|
|
455
|
+
parseComments(item.data.replies.data.children, depth + 1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
if (json[1] && json[1].data && json[1].data.children) {
|
|
461
|
+
parseComments(json[1].data.children);
|
|
462
|
+
}
|
|
463
|
+
return { post, comments };
|
|
464
|
+
} catch {
|
|
465
|
+
throw new Error(`Failed to get comments for post ${postId}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async getUserPosts(username, options = {}) {
|
|
469
|
+
await this.authenticate();
|
|
470
|
+
try {
|
|
471
|
+
const { sort = "new", timeFilter = "all", limit = 25 } = options;
|
|
472
|
+
const params = new URLSearchParams({
|
|
473
|
+
sort,
|
|
474
|
+
t: timeFilter,
|
|
475
|
+
limit: limit.toString()
|
|
476
|
+
});
|
|
477
|
+
const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
|
|
478
|
+
if (!response.ok) {
|
|
479
|
+
throw new Error(`HTTP ${response.status}`);
|
|
480
|
+
}
|
|
481
|
+
const json = await response.json();
|
|
482
|
+
return json.data.children.filter((child) => child.kind === "t3").map((child) => {
|
|
483
|
+
const post = child.data;
|
|
484
|
+
return {
|
|
485
|
+
id: post.id,
|
|
486
|
+
title: post.title,
|
|
487
|
+
author: post.author,
|
|
488
|
+
subreddit: post.subreddit,
|
|
489
|
+
selftext: post.selftext || "",
|
|
490
|
+
url: post.url,
|
|
491
|
+
score: post.score,
|
|
492
|
+
upvoteRatio: post.upvote_ratio,
|
|
493
|
+
numComments: post.num_comments,
|
|
494
|
+
createdUtc: post.created_utc,
|
|
495
|
+
over18: post.over_18,
|
|
496
|
+
spoiler: post.spoiler,
|
|
497
|
+
edited: !!post.edited,
|
|
498
|
+
isSelf: post.is_self,
|
|
499
|
+
linkFlairText: post.link_flair_text,
|
|
500
|
+
permalink: post.permalink
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
} catch {
|
|
504
|
+
throw new Error(`Failed to get posts for user ${username}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async getUserComments(username, options = {}) {
|
|
508
|
+
await this.authenticate();
|
|
509
|
+
try {
|
|
510
|
+
const { sort = "new", timeFilter = "all", limit = 25 } = options;
|
|
511
|
+
const params = new URLSearchParams({
|
|
512
|
+
sort,
|
|
513
|
+
t: timeFilter,
|
|
514
|
+
limit: limit.toString()
|
|
515
|
+
});
|
|
516
|
+
const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
throw new Error(`HTTP ${response.status}`);
|
|
519
|
+
}
|
|
520
|
+
const json = await response.json();
|
|
521
|
+
return json.data.children.filter((child) => child.kind === "t1").map((child) => {
|
|
522
|
+
const comment = child.data;
|
|
523
|
+
return {
|
|
524
|
+
id: comment.id,
|
|
525
|
+
author: comment.author,
|
|
526
|
+
body: comment.body,
|
|
527
|
+
score: comment.score,
|
|
528
|
+
controversiality: comment.controversiality,
|
|
529
|
+
subreddit: comment.subreddit,
|
|
530
|
+
submissionTitle: comment.link_title || "",
|
|
531
|
+
createdUtc: comment.created_utc,
|
|
532
|
+
edited: !!comment.edited,
|
|
533
|
+
isSubmitter: comment.is_submitter,
|
|
534
|
+
permalink: comment.permalink
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
} catch {
|
|
538
|
+
throw new Error(`Failed to get comments for user ${username}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
316
541
|
};
|
|
317
542
|
var redditClient = null;
|
|
318
543
|
function initializeRedditClient(config) {
|
|
@@ -557,6 +782,21 @@ function formatCommentInfo(comment) {
|
|
|
557
782
|
commentAnalysis: analyzeCommentImpact(comment.score, comment.edited, comment.isSubmitter)
|
|
558
783
|
};
|
|
559
784
|
}
|
|
785
|
+
function formatPost(post) {
|
|
786
|
+
return {
|
|
787
|
+
title: post.title,
|
|
788
|
+
author: post.author,
|
|
789
|
+
subreddit: post.subreddit,
|
|
790
|
+
score: post.score,
|
|
791
|
+
upvoteRatio: Math.round(post.upvoteRatio * 100),
|
|
792
|
+
numComments: post.numComments,
|
|
793
|
+
createdAt: formatTimestamp(post.createdUtc),
|
|
794
|
+
selftext: post.selftext,
|
|
795
|
+
permalink: post.permalink,
|
|
796
|
+
nsfw: post.over18,
|
|
797
|
+
spoiler: post.spoiler
|
|
798
|
+
};
|
|
799
|
+
}
|
|
560
800
|
|
|
561
801
|
// src/tools/user-tools.ts
|
|
562
802
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -599,6 +839,86 @@ async function getUserInfo(params) {
|
|
|
599
839
|
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user data: ${String(error)}`);
|
|
600
840
|
}
|
|
601
841
|
}
|
|
842
|
+
async function getUserPosts(params) {
|
|
843
|
+
const { username, sort = "new", time_filter = "all", limit = 10 } = params;
|
|
844
|
+
const client = getRedditClient();
|
|
845
|
+
if (!client) {
|
|
846
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const posts = await client.getUserPosts(username, {
|
|
850
|
+
sort,
|
|
851
|
+
timeFilter: time_filter,
|
|
852
|
+
limit
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
content: [
|
|
856
|
+
{
|
|
857
|
+
type: "text",
|
|
858
|
+
text: `# Posts by u/${username}
|
|
859
|
+
|
|
860
|
+
## Sort: ${sort} | Time: ${time_filter} | Count: ${posts.length}
|
|
861
|
+
|
|
862
|
+
${posts.map((post, index) => {
|
|
863
|
+
const date = new Date(post.createdUtc * 1e3).toLocaleString();
|
|
864
|
+
const selftext = post.selftext ? `
|
|
865
|
+
${post.selftext.substring(0, 200)}${post.selftext.length > 200 ? "..." : ""}
|
|
866
|
+
` : "";
|
|
867
|
+
return `### ${index + 1}. ${post.title}
|
|
868
|
+
- Subreddit: r/${post.subreddit}
|
|
869
|
+
- Score: ${post.score} (${Math.round(post.upvoteRatio * 100)}% upvoted)
|
|
870
|
+
- Comments: ${post.numComments}
|
|
871
|
+
- Posted: ${date}
|
|
872
|
+
${selftext}
|
|
873
|
+
- Link: https://reddit.com${post.permalink}
|
|
874
|
+
${post.over18 ? "- **NSFW**" : ""}
|
|
875
|
+
${post.spoiler ? "- **Spoiler**" : ""}`;
|
|
876
|
+
}).join("\n\n---\n\n")}`
|
|
877
|
+
}
|
|
878
|
+
]
|
|
879
|
+
};
|
|
880
|
+
} catch (error) {
|
|
881
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user posts: ${String(error)}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function getUserComments(params) {
|
|
885
|
+
const { username, sort = "new", time_filter = "all", limit = 10 } = params;
|
|
886
|
+
const client = getRedditClient();
|
|
887
|
+
if (!client) {
|
|
888
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const comments = await client.getUserComments(username, {
|
|
892
|
+
sort,
|
|
893
|
+
timeFilter: time_filter,
|
|
894
|
+
limit
|
|
895
|
+
});
|
|
896
|
+
return {
|
|
897
|
+
content: [
|
|
898
|
+
{
|
|
899
|
+
type: "text",
|
|
900
|
+
text: `# Comments by u/${username}
|
|
901
|
+
|
|
902
|
+
## Sort: ${sort} | Time: ${time_filter} | Count: ${comments.length}
|
|
903
|
+
|
|
904
|
+
${comments.map((comment, index) => {
|
|
905
|
+
const date = new Date(comment.createdUtc * 1e3).toLocaleString();
|
|
906
|
+
const edited = comment.edited ? " *(edited)*" : "";
|
|
907
|
+
const body = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body;
|
|
908
|
+
return `### ${index + 1}. In r/${comment.subreddit} on "${comment.submissionTitle}"
|
|
909
|
+
- Score: ${comment.score} points
|
|
910
|
+
- Posted: ${date}${edited}
|
|
911
|
+
- Link: https://reddit.com${comment.permalink}
|
|
912
|
+
|
|
913
|
+
${body}`;
|
|
914
|
+
}).join("\n\n---\n\n")}`
|
|
915
|
+
}
|
|
916
|
+
]
|
|
917
|
+
};
|
|
918
|
+
} catch (error) {
|
|
919
|
+
throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user comments: ${String(error)}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
602
922
|
|
|
603
923
|
// src/tools/post-tools.ts
|
|
604
924
|
var import_types2 = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -826,6 +1146,114 @@ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).j
|
|
|
826
1146
|
}
|
|
827
1147
|
}
|
|
828
1148
|
|
|
1149
|
+
// src/tools/search-tools.ts
|
|
1150
|
+
var import_types4 = require("@modelcontextprotocol/sdk/types.js");
|
|
1151
|
+
async function searchReddit(params) {
|
|
1152
|
+
const { query, subreddit, sort = "relevance", time_filter = "all", limit = 10, type = "link" } = params;
|
|
1153
|
+
const client = getRedditClient();
|
|
1154
|
+
if (!client) {
|
|
1155
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InternalError, "Reddit client not initialized");
|
|
1156
|
+
}
|
|
1157
|
+
if (!query || query.trim().length === 0) {
|
|
1158
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, "Search query cannot be empty");
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
const posts = await client.searchReddit(query, {
|
|
1162
|
+
subreddit,
|
|
1163
|
+
sort,
|
|
1164
|
+
timeFilter: time_filter,
|
|
1165
|
+
limit,
|
|
1166
|
+
type
|
|
1167
|
+
});
|
|
1168
|
+
return {
|
|
1169
|
+
content: [
|
|
1170
|
+
{
|
|
1171
|
+
type: "text",
|
|
1172
|
+
text: `# Reddit Search Results for: "${query}"${subreddit ? ` in r/${subreddit}` : ""}
|
|
1173
|
+
|
|
1174
|
+
## Search Parameters
|
|
1175
|
+
- Sort: ${sort}
|
|
1176
|
+
- Time Filter: ${time_filter}
|
|
1177
|
+
- Type: ${type}
|
|
1178
|
+
- Results: ${posts.length}
|
|
1179
|
+
|
|
1180
|
+
${posts.map((post, index) => {
|
|
1181
|
+
const formatted = formatPost(post);
|
|
1182
|
+
return `### ${index + 1}. ${formatted.title}
|
|
1183
|
+
- Author: u/${formatted.author}
|
|
1184
|
+
- Subreddit: r/${formatted.subreddit}
|
|
1185
|
+
- Score: ${formatted.score} (${formatted.upvoteRatio}% upvoted)
|
|
1186
|
+
- Comments: ${formatted.numComments}
|
|
1187
|
+
- Posted: ${formatted.createdAt}
|
|
1188
|
+
${formatted.selftext ? `
|
|
1189
|
+
${formatted.selftext.substring(0, 200)}${formatted.selftext.length > 200 ? "..." : ""}
|
|
1190
|
+
` : ""}
|
|
1191
|
+
- Link: https://reddit.com${formatted.permalink}
|
|
1192
|
+
${formatted.nsfw ? "- **NSFW**" : ""}
|
|
1193
|
+
${formatted.spoiler ? "- **Spoiler**" : ""}
|
|
1194
|
+
`;
|
|
1195
|
+
}).join("\n")}`
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
};
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
throw new import_types4.McpError(import_types4.ErrorCode.InternalError, `Failed to search Reddit: ${String(error)}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/tools/comment-tools.ts
|
|
1205
|
+
var import_types5 = require("@modelcontextprotocol/sdk/types.js");
|
|
1206
|
+
async function getPostComments(params) {
|
|
1207
|
+
const { post_id, subreddit, sort = "best", limit = 100 } = params;
|
|
1208
|
+
const client = getRedditClient();
|
|
1209
|
+
if (!client) {
|
|
1210
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InternalError, "Reddit client not initialized");
|
|
1211
|
+
}
|
|
1212
|
+
if (!post_id || !subreddit) {
|
|
1213
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InvalidParams, "post_id and subreddit are required");
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const { post, comments } = await client.getPostComments(post_id, subreddit, {
|
|
1217
|
+
sort,
|
|
1218
|
+
limit
|
|
1219
|
+
});
|
|
1220
|
+
const formattedPost = formatPost(post);
|
|
1221
|
+
const formatComment = (comment) => {
|
|
1222
|
+
const edited = comment.edited ? " *(edited)*" : "";
|
|
1223
|
+
const submitter = comment.isSubmitter ? " **[OP]**" : "";
|
|
1224
|
+
const depth = comment.depth || 0;
|
|
1225
|
+
const prefix = " ".repeat(depth) + (depth > 0 ? "\u2514\u2500 " : "");
|
|
1226
|
+
return `${prefix}**u/${comment.author}**${submitter} \u2022 ${comment.score} points \u2022 ${new Date(comment.createdUtc * 1e3).toLocaleString()}${edited}
|
|
1227
|
+
${prefix}${comment.body.split("\n").join(`
|
|
1228
|
+
${prefix}`)}`;
|
|
1229
|
+
};
|
|
1230
|
+
return {
|
|
1231
|
+
content: [
|
|
1232
|
+
{
|
|
1233
|
+
type: "text",
|
|
1234
|
+
text: `# Comments for: ${formattedPost.title}
|
|
1235
|
+
|
|
1236
|
+
## Post Details
|
|
1237
|
+
- Author: u/${formattedPost.author}
|
|
1238
|
+
- Subreddit: r/${formattedPost.subreddit}
|
|
1239
|
+
- Score: ${formattedPost.score} (${formattedPost.upvoteRatio}% upvoted)
|
|
1240
|
+
- Posted: ${formattedPost.createdAt}
|
|
1241
|
+
- Link: https://reddit.com${formattedPost.permalink}
|
|
1242
|
+
|
|
1243
|
+
## Post Content
|
|
1244
|
+
${formattedPost.selftext || "[Link post - no text content]"}
|
|
1245
|
+
|
|
1246
|
+
## Comments (${comments.length} loaded, sorted by ${sort})
|
|
1247
|
+
|
|
1248
|
+
${comments.map((comment) => formatComment(comment)).join("\n\n---\n\n")}`
|
|
1249
|
+
}
|
|
1250
|
+
]
|
|
1251
|
+
};
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
throw new import_types5.McpError(import_types5.ErrorCode.InternalError, `Failed to fetch comments: ${String(error)}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
829
1257
|
// src/index.ts
|
|
830
1258
|
var import_dotenv = __toESM(require("dotenv"));
|
|
831
1259
|
import_dotenv.default.config();
|
|
@@ -864,9 +1292,6 @@ var RedditServer = class {
|
|
|
864
1292
|
const username = process.env.REDDIT_USERNAME;
|
|
865
1293
|
const password = process.env.REDDIT_PASSWORD;
|
|
866
1294
|
if (!clientId || !clientSecret) {
|
|
867
|
-
console.error(
|
|
868
|
-
"[Error] Missing required Reddit API credentials. Please set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables."
|
|
869
|
-
);
|
|
870
1295
|
process.exit(1);
|
|
871
1296
|
}
|
|
872
1297
|
try {
|
|
@@ -877,13 +1302,12 @@ var RedditServer = class {
|
|
|
877
1302
|
username,
|
|
878
1303
|
password
|
|
879
1304
|
});
|
|
880
|
-
} catch
|
|
881
|
-
console.error("[Error] Failed to initialize Reddit client:", error);
|
|
1305
|
+
} catch {
|
|
882
1306
|
process.exit(1);
|
|
883
1307
|
}
|
|
884
1308
|
}
|
|
885
1309
|
setupToolHandlers() {
|
|
886
|
-
this.server.setRequestHandler(
|
|
1310
|
+
this.server.setRequestHandler(import_types6.ListToolsRequestSchema, async () => ({
|
|
887
1311
|
tools: [
|
|
888
1312
|
{
|
|
889
1313
|
name: "test_reddit_mcp_server",
|
|
@@ -1022,10 +1446,150 @@ var RedditServer = class {
|
|
|
1022
1446
|
},
|
|
1023
1447
|
required: ["post_id", "content"]
|
|
1024
1448
|
}
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: "search_reddit",
|
|
1452
|
+
description: "Search for posts on Reddit",
|
|
1453
|
+
inputSchema: {
|
|
1454
|
+
type: "object",
|
|
1455
|
+
properties: {
|
|
1456
|
+
query: {
|
|
1457
|
+
type: "string",
|
|
1458
|
+
description: "The search query"
|
|
1459
|
+
},
|
|
1460
|
+
subreddit: {
|
|
1461
|
+
type: "string",
|
|
1462
|
+
description: "Search within a specific subreddit (optional)"
|
|
1463
|
+
},
|
|
1464
|
+
sort: {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "Sort order: relevance, hot, top, new, comments",
|
|
1467
|
+
enum: ["relevance", "hot", "top", "new", "comments"],
|
|
1468
|
+
default: "relevance"
|
|
1469
|
+
},
|
|
1470
|
+
time_filter: {
|
|
1471
|
+
type: "string",
|
|
1472
|
+
description: "Time filter: hour, day, week, month, year, all",
|
|
1473
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1474
|
+
default: "all"
|
|
1475
|
+
},
|
|
1476
|
+
limit: {
|
|
1477
|
+
type: "number",
|
|
1478
|
+
description: "Maximum number of results to return",
|
|
1479
|
+
minimum: 1,
|
|
1480
|
+
maximum: 100,
|
|
1481
|
+
default: 10
|
|
1482
|
+
},
|
|
1483
|
+
type: {
|
|
1484
|
+
type: "string",
|
|
1485
|
+
description: "Type of content: link (posts), sr (subreddits), user (users)",
|
|
1486
|
+
enum: ["link", "sr", "user"],
|
|
1487
|
+
default: "link"
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
required: ["query"]
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
name: "get_post_comments",
|
|
1495
|
+
description: "Get comments for a specific Reddit post",
|
|
1496
|
+
inputSchema: {
|
|
1497
|
+
type: "object",
|
|
1498
|
+
properties: {
|
|
1499
|
+
post_id: {
|
|
1500
|
+
type: "string",
|
|
1501
|
+
description: "The ID of the post"
|
|
1502
|
+
},
|
|
1503
|
+
subreddit: {
|
|
1504
|
+
type: "string",
|
|
1505
|
+
description: "The subreddit where the post is located"
|
|
1506
|
+
},
|
|
1507
|
+
sort: {
|
|
1508
|
+
type: "string",
|
|
1509
|
+
description: "Comment sort order: best, top, new, controversial, old, qa",
|
|
1510
|
+
enum: ["best", "top", "new", "controversial", "old", "qa"],
|
|
1511
|
+
default: "best"
|
|
1512
|
+
},
|
|
1513
|
+
limit: {
|
|
1514
|
+
type: "number",
|
|
1515
|
+
description: "Maximum number of comments to load",
|
|
1516
|
+
minimum: 1,
|
|
1517
|
+
maximum: 500,
|
|
1518
|
+
default: 100
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
required: ["post_id", "subreddit"]
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
name: "get_user_posts",
|
|
1526
|
+
description: "Get posts submitted by a specific user",
|
|
1527
|
+
inputSchema: {
|
|
1528
|
+
type: "object",
|
|
1529
|
+
properties: {
|
|
1530
|
+
username: {
|
|
1531
|
+
type: "string",
|
|
1532
|
+
description: "The username to get posts for"
|
|
1533
|
+
},
|
|
1534
|
+
sort: {
|
|
1535
|
+
type: "string",
|
|
1536
|
+
description: "Sort order: new, hot, top, controversial",
|
|
1537
|
+
enum: ["new", "hot", "top", "controversial"],
|
|
1538
|
+
default: "new"
|
|
1539
|
+
},
|
|
1540
|
+
time_filter: {
|
|
1541
|
+
type: "string",
|
|
1542
|
+
description: "Time filter for top/controversial: hour, day, week, month, year, all",
|
|
1543
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1544
|
+
default: "all"
|
|
1545
|
+
},
|
|
1546
|
+
limit: {
|
|
1547
|
+
type: "number",
|
|
1548
|
+
description: "Maximum number of posts to return",
|
|
1549
|
+
minimum: 1,
|
|
1550
|
+
maximum: 100,
|
|
1551
|
+
default: 10
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
required: ["username"]
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
name: "get_user_comments",
|
|
1559
|
+
description: "Get comments made by a specific user",
|
|
1560
|
+
inputSchema: {
|
|
1561
|
+
type: "object",
|
|
1562
|
+
properties: {
|
|
1563
|
+
username: {
|
|
1564
|
+
type: "string",
|
|
1565
|
+
description: "The username to get comments for"
|
|
1566
|
+
},
|
|
1567
|
+
sort: {
|
|
1568
|
+
type: "string",
|
|
1569
|
+
description: "Sort order: new, hot, top, controversial",
|
|
1570
|
+
enum: ["new", "hot", "top", "controversial"],
|
|
1571
|
+
default: "new"
|
|
1572
|
+
},
|
|
1573
|
+
time_filter: {
|
|
1574
|
+
type: "string",
|
|
1575
|
+
description: "Time filter for top/controversial: hour, day, week, month, year, all",
|
|
1576
|
+
enum: ["hour", "day", "week", "month", "year", "all"],
|
|
1577
|
+
default: "all"
|
|
1578
|
+
},
|
|
1579
|
+
limit: {
|
|
1580
|
+
type: "number",
|
|
1581
|
+
description: "Maximum number of comments to return",
|
|
1582
|
+
minimum: 1,
|
|
1583
|
+
maximum: 100,
|
|
1584
|
+
default: 10
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
required: ["username"]
|
|
1588
|
+
}
|
|
1025
1589
|
}
|
|
1026
1590
|
]
|
|
1027
1591
|
}));
|
|
1028
|
-
this.server.setRequestHandler(
|
|
1592
|
+
this.server.setRequestHandler(import_types6.CallToolRequestSchema, async (request) => {
|
|
1029
1593
|
try {
|
|
1030
1594
|
const toolName = request.params.name;
|
|
1031
1595
|
const toolParams = request.params.arguments || {};
|
|
@@ -1064,8 +1628,24 @@ var RedditServer = class {
|
|
|
1064
1628
|
return await replyToPost(
|
|
1065
1629
|
toolParams
|
|
1066
1630
|
);
|
|
1631
|
+
case "search_reddit":
|
|
1632
|
+
return await searchReddit(
|
|
1633
|
+
toolParams
|
|
1634
|
+
);
|
|
1635
|
+
case "get_post_comments":
|
|
1636
|
+
return await getPostComments(
|
|
1637
|
+
toolParams
|
|
1638
|
+
);
|
|
1639
|
+
case "get_user_posts":
|
|
1640
|
+
return await getUserPosts(
|
|
1641
|
+
toolParams
|
|
1642
|
+
);
|
|
1643
|
+
case "get_user_comments":
|
|
1644
|
+
return await getUserComments(
|
|
1645
|
+
toolParams
|
|
1646
|
+
);
|
|
1067
1647
|
default:
|
|
1068
|
-
throw new
|
|
1648
|
+
throw new import_types6.McpError(import_types6.ErrorCode.MethodNotFound, `Tool with name ${toolName} not found`);
|
|
1069
1649
|
}
|
|
1070
1650
|
} catch (error) {
|
|
1071
1651
|
if (error instanceof Error) {
|
|
@@ -1074,7 +1654,7 @@ var RedditServer = class {
|
|
|
1074
1654
|
logger: "reddit-server",
|
|
1075
1655
|
data: `Error calling tool: ${error.message}`
|
|
1076
1656
|
});
|
|
1077
|
-
throw new
|
|
1657
|
+
throw new import_types6.McpError(import_types6.ErrorCode.InternalError, `Failed to fetch data: ${error.message}`);
|
|
1078
1658
|
}
|
|
1079
1659
|
throw error;
|
|
1080
1660
|
}
|
|
@@ -1099,7 +1679,9 @@ var RedditServer = class {
|
|
|
1099
1679
|
};
|
|
1100
1680
|
if (require.main === module) {
|
|
1101
1681
|
const server = new RedditServer();
|
|
1102
|
-
server.run().catch(
|
|
1682
|
+
server.run().catch(() => {
|
|
1683
|
+
process.exit(1);
|
|
1684
|
+
});
|
|
1103
1685
|
}
|
|
1104
1686
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1105
1687
|
0 && (module.exports = {
|