reddit-mcp-server 1.0.9 → 1.1.0

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/dist/server.js ADDED
@@ -0,0 +1,1754 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ startServer: () => startServer
34
+ });
35
+ module.exports = __toCommonJS(server_exports);
36
+ var import_hono = require("hono");
37
+ var import_cors = require("hono/cors");
38
+ var import_node_server = require("@hono/node-server");
39
+ var import_mcp = require("@hono/mcp");
40
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
41
+ var import_types6 = require("@modelcontextprotocol/sdk/types.js");
42
+
43
+ // src/client/reddit-client.ts
44
+ var RedditClient = class {
45
+ clientId;
46
+ clientSecret;
47
+ userAgent;
48
+ username;
49
+ password;
50
+ accessToken;
51
+ tokenExpiry = 0;
52
+ baseUrl = "https://oauth.reddit.com";
53
+ authenticated = false;
54
+ constructor(config) {
55
+ this.clientId = config.clientId;
56
+ this.clientSecret = config.clientSecret;
57
+ this.userAgent = config.userAgent;
58
+ this.username = config.username;
59
+ this.password = config.password;
60
+ }
61
+ async makeRequest(path, options = {}) {
62
+ if (Date.now() >= this.tokenExpiry || !this.authenticated) {
63
+ await this.authenticate();
64
+ }
65
+ const url = `${this.baseUrl}${path}`;
66
+ const headers = {
67
+ "User-Agent": this.userAgent,
68
+ Authorization: `Bearer ${this.accessToken}`,
69
+ ...options.headers
70
+ };
71
+ const response = await fetch(url, {
72
+ ...options,
73
+ headers
74
+ });
75
+ if (response.status === 401 && this.authenticated) {
76
+ await this.authenticate();
77
+ const retryHeaders = {
78
+ ...headers,
79
+ Authorization: `Bearer ${this.accessToken}`
80
+ };
81
+ return fetch(url, {
82
+ ...options,
83
+ headers: retryHeaders
84
+ });
85
+ }
86
+ return response;
87
+ }
88
+ async authenticate() {
89
+ try {
90
+ const now = Date.now();
91
+ if (this.accessToken && now < this.tokenExpiry) {
92
+ return;
93
+ }
94
+ const authUrl = "https://www.reddit.com/api/v1/access_token";
95
+ const authData = new URLSearchParams();
96
+ if (this.username && this.password) {
97
+ authData.append("grant_type", "password");
98
+ authData.append("username", this.username);
99
+ authData.append("password", this.password);
100
+ } else {
101
+ authData.append("grant_type", "client_credentials");
102
+ }
103
+ const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
104
+ const response = await fetch(authUrl, {
105
+ method: "POST",
106
+ headers: {
107
+ "User-Agent": this.userAgent,
108
+ "Content-Type": "application/x-www-form-urlencoded",
109
+ Authorization: `Basic ${credentials}`
110
+ },
111
+ body: authData.toString()
112
+ });
113
+ if (!response.ok) {
114
+ throw new Error(`Authentication failed: ${response.status}`);
115
+ }
116
+ const data = await response.json();
117
+ this.accessToken = data.access_token;
118
+ this.tokenExpiry = now + data.expires_in * 1e3;
119
+ this.authenticated = true;
120
+ } catch {
121
+ throw new Error("Failed to authenticate with Reddit API");
122
+ }
123
+ }
124
+ async checkAuthentication() {
125
+ if (!this.authenticated) {
126
+ try {
127
+ await this.authenticate();
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ }
135
+ async getUser(username) {
136
+ await this.authenticate();
137
+ try {
138
+ const response = await this.makeRequest(`/user/${username}/about.json`);
139
+ if (!response.ok) {
140
+ throw new Error(`HTTP ${response.status}`);
141
+ }
142
+ const json = await response.json();
143
+ const data = json.data;
144
+ return {
145
+ name: data.name,
146
+ id: data.id,
147
+ commentKarma: data.comment_karma,
148
+ linkKarma: data.link_karma,
149
+ totalKarma: data.total_karma || data.comment_karma + data.link_karma,
150
+ isMod: data.is_mod,
151
+ isGold: data.is_gold,
152
+ isEmployee: data.is_employee,
153
+ createdUtc: data.created_utc,
154
+ profileUrl: `https://reddit.com/user/${data.name}`
155
+ };
156
+ } catch {
157
+ throw new Error(`Failed to get user info for ${username}`);
158
+ }
159
+ }
160
+ async getSubredditInfo(subredditName) {
161
+ await this.authenticate();
162
+ try {
163
+ const response = await this.makeRequest(`/r/${subredditName}/about.json`);
164
+ if (!response.ok) {
165
+ throw new Error(`HTTP ${response.status}`);
166
+ }
167
+ const json = await response.json();
168
+ const data = json.data;
169
+ return {
170
+ displayName: data.display_name,
171
+ title: data.title,
172
+ description: data.description || "",
173
+ publicDescription: data.public_description || "",
174
+ subscribers: data.subscribers,
175
+ activeUserCount: data.active_user_count ?? void 0,
176
+ createdUtc: data.created_utc,
177
+ over18: data.over18,
178
+ subredditType: data.subreddit_type,
179
+ url: data.url
180
+ };
181
+ } catch {
182
+ throw new Error(`Failed to get subreddit info for ${subredditName}`);
183
+ }
184
+ }
185
+ async getTopPosts(subreddit, timeFilter = "week", limit = 10) {
186
+ await this.authenticate();
187
+ try {
188
+ const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
189
+ const params = new URLSearchParams({
190
+ t: timeFilter,
191
+ limit: limit.toString()
192
+ });
193
+ const response = await this.makeRequest(`${endpoint}?${params}`);
194
+ if (!response.ok) {
195
+ throw new Error(`HTTP ${response.status}`);
196
+ }
197
+ const json = await response.json();
198
+ return json.data.children.map((child) => {
199
+ const post = child.data;
200
+ return {
201
+ id: post.id,
202
+ title: post.title,
203
+ author: post.author,
204
+ subreddit: post.subreddit,
205
+ selftext: post.selftext,
206
+ url: post.url,
207
+ score: post.score,
208
+ upvoteRatio: post.upvote_ratio,
209
+ numComments: post.num_comments,
210
+ createdUtc: post.created_utc,
211
+ over18: post.over_18,
212
+ spoiler: post.spoiler,
213
+ edited: !!post.edited,
214
+ isSelf: post.is_self,
215
+ linkFlairText: post.link_flair_text ?? void 0,
216
+ permalink: post.permalink
217
+ };
218
+ });
219
+ } catch {
220
+ throw new Error(`Failed to get top posts for ${subreddit || "home"}`);
221
+ }
222
+ }
223
+ async getPost(postId, subreddit) {
224
+ await this.authenticate();
225
+ try {
226
+ const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
227
+ const response = await this.makeRequest(endpoint);
228
+ if (!response.ok) {
229
+ throw new Error(`HTTP ${response.status}`);
230
+ }
231
+ const json = await response.json();
232
+ let post;
233
+ if (subreddit) {
234
+ post = json[0].data.children[0].data;
235
+ } else {
236
+ if (!json.data.children.length) {
237
+ throw new Error(`Post with ID ${postId} not found`);
238
+ }
239
+ post = json.data.children[0].data;
240
+ }
241
+ return {
242
+ id: post.id,
243
+ title: post.title,
244
+ author: post.author,
245
+ subreddit: post.subreddit,
246
+ selftext: post.selftext,
247
+ url: post.url,
248
+ score: post.score,
249
+ upvoteRatio: post.upvote_ratio,
250
+ numComments: post.num_comments,
251
+ createdUtc: post.created_utc,
252
+ over18: post.over_18,
253
+ spoiler: post.spoiler,
254
+ edited: !!post.edited,
255
+ isSelf: post.is_self,
256
+ linkFlairText: post.link_flair_text,
257
+ permalink: post.permalink
258
+ };
259
+ } catch {
260
+ throw new Error(`Failed to get post with ID ${postId}`);
261
+ }
262
+ }
263
+ async getTrendingSubreddits(limit = 5) {
264
+ await this.authenticate();
265
+ try {
266
+ const params = new URLSearchParams({ limit: limit.toString() });
267
+ const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
268
+ if (!response.ok) {
269
+ throw new Error(`HTTP ${response.status}`);
270
+ }
271
+ const json = await response.json();
272
+ return json.data.children.map((child) => child.data.display_name);
273
+ } catch {
274
+ throw new Error("Failed to get trending subreddits");
275
+ }
276
+ }
277
+ async createPost(subreddit, title, content, isSelf = true) {
278
+ await this.authenticate();
279
+ if (!this.username || !this.password) {
280
+ throw new Error("User authentication required for posting");
281
+ }
282
+ try {
283
+ const kind = isSelf ? "self" : "link";
284
+ const params = new URLSearchParams();
285
+ params.append("sr", subreddit);
286
+ params.append("kind", kind);
287
+ params.append("title", title);
288
+ params.append(isSelf ? "text" : "url", content);
289
+ const response = await this.makeRequest("/api/submit", {
290
+ method: "POST",
291
+ headers: {
292
+ "Content-Type": "application/x-www-form-urlencoded"
293
+ },
294
+ body: params.toString()
295
+ });
296
+ if (!response.ok) {
297
+ throw new Error(`HTTP ${response.status}`);
298
+ }
299
+ const json = await response.json();
300
+ if (json.success) {
301
+ const postId = json.data.id;
302
+ return await this.getPost(postId);
303
+ } else {
304
+ throw new Error("Failed to create post");
305
+ }
306
+ } catch {
307
+ throw new Error(`Failed to create post in ${subreddit}`);
308
+ }
309
+ }
310
+ async checkPostExists(postId) {
311
+ await this.authenticate();
312
+ try {
313
+ const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
314
+ if (!response.ok) {
315
+ return false;
316
+ }
317
+ const json = await response.json();
318
+ return json.data.children.length > 0;
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+ async replyToPost(postId, content) {
324
+ await this.authenticate();
325
+ if (!this.username || !this.password) {
326
+ throw new Error("User authentication required for posting replies");
327
+ }
328
+ try {
329
+ if (!await this.checkPostExists(postId)) {
330
+ throw new Error(`Post with ID ${postId} does not exist or is not accessible`);
331
+ }
332
+ const params = new URLSearchParams();
333
+ params.append("thing_id", `t3_${postId}`);
334
+ params.append("text", content);
335
+ const response = await this.makeRequest("/api/comment", {
336
+ method: "POST",
337
+ headers: {
338
+ "Content-Type": "application/x-www-form-urlencoded"
339
+ },
340
+ body: params.toString()
341
+ });
342
+ if (!response.ok) {
343
+ throw new Error(`HTTP ${response.status}`);
344
+ }
345
+ const commentData = await response.json();
346
+ return {
347
+ id: commentData.id,
348
+ author: this.username,
349
+ body: content,
350
+ score: 1,
351
+ controversiality: 0,
352
+ subreddit: commentData.subreddit,
353
+ submissionTitle: commentData.link_title,
354
+ createdUtc: Date.now() / 1e3,
355
+ edited: false,
356
+ isSubmitter: false,
357
+ permalink: commentData.permalink
358
+ };
359
+ } catch {
360
+ throw new Error(`Failed to reply to post ${postId}`);
361
+ }
362
+ }
363
+ async searchReddit(query, options = {}) {
364
+ await this.authenticate();
365
+ try {
366
+ const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
367
+ const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
368
+ const params = new URLSearchParams({
369
+ q: query,
370
+ sort,
371
+ t: timeFilter,
372
+ limit: limit.toString(),
373
+ type,
374
+ ...subreddit && { restrict_sr: "true" }
375
+ });
376
+ const response = await this.makeRequest(`${endpoint}?${params}`);
377
+ if (!response.ok) {
378
+ throw new Error(`HTTP ${response.status}`);
379
+ }
380
+ const json = await response.json();
381
+ return json.data.children.filter((child) => child.kind === "t3").map((child) => {
382
+ const post = child.data;
383
+ return {
384
+ id: post.id,
385
+ title: post.title,
386
+ author: post.author,
387
+ subreddit: post.subreddit,
388
+ selftext: post.selftext || "",
389
+ url: post.url,
390
+ score: post.score,
391
+ upvoteRatio: post.upvote_ratio,
392
+ numComments: post.num_comments,
393
+ createdUtc: post.created_utc,
394
+ over18: post.over_18,
395
+ spoiler: post.spoiler,
396
+ edited: !!post.edited,
397
+ isSelf: post.is_self,
398
+ linkFlairText: post.link_flair_text ?? void 0,
399
+ permalink: post.permalink
400
+ };
401
+ });
402
+ } catch {
403
+ throw new Error(`Failed to search Reddit for: ${query}`);
404
+ }
405
+ }
406
+ async getPostComments(postId, subreddit, options = {}) {
407
+ await this.authenticate();
408
+ try {
409
+ const { sort = "best", limit = 100 } = options;
410
+ const params = new URLSearchParams({
411
+ sort,
412
+ limit: limit.toString()
413
+ });
414
+ const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
415
+ if (!response.ok) {
416
+ throw new Error(`HTTP ${response.status}`);
417
+ }
418
+ const json = await response.json();
419
+ const postData = json[0].data.children[0].data;
420
+ const post = {
421
+ id: postData.id,
422
+ title: postData.title,
423
+ author: postData.author,
424
+ subreddit: postData.subreddit,
425
+ selftext: postData.selftext || "",
426
+ url: postData.url,
427
+ score: postData.score,
428
+ upvoteRatio: postData.upvote_ratio,
429
+ numComments: postData.num_comments,
430
+ createdUtc: postData.created_utc,
431
+ over18: postData.over_18,
432
+ spoiler: postData.spoiler,
433
+ edited: !!postData.edited,
434
+ isSelf: postData.is_self,
435
+ linkFlairText: postData.link_flair_text,
436
+ permalink: postData.permalink
437
+ };
438
+ const comments = [];
439
+ const parseComments = (commentData, depth = 0) => {
440
+ for (const item of commentData) {
441
+ if (item.kind === "t1" && item.data.body) {
442
+ comments.push({
443
+ id: item.data.id,
444
+ author: item.data.author,
445
+ body: item.data.body,
446
+ score: item.data.score,
447
+ controversiality: item.data.controversiality,
448
+ subreddit: item.data.subreddit,
449
+ submissionTitle: post.title,
450
+ createdUtc: item.data.created_utc,
451
+ edited: !!item.data.edited,
452
+ isSubmitter: item.data.is_submitter,
453
+ permalink: item.data.permalink,
454
+ depth,
455
+ parentId: item.data.parent_id
456
+ });
457
+ if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
458
+ parseComments(item.data.replies.data.children, depth + 1);
459
+ }
460
+ }
461
+ }
462
+ };
463
+ if (json[1] && json[1].data && json[1].data.children) {
464
+ parseComments(json[1].data.children);
465
+ }
466
+ return { post, comments };
467
+ } catch {
468
+ throw new Error(`Failed to get comments for post ${postId}`);
469
+ }
470
+ }
471
+ async getUserPosts(username, options = {}) {
472
+ await this.authenticate();
473
+ try {
474
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
475
+ const params = new URLSearchParams({
476
+ sort,
477
+ t: timeFilter,
478
+ limit: limit.toString()
479
+ });
480
+ const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
481
+ if (!response.ok) {
482
+ throw new Error(`HTTP ${response.status}`);
483
+ }
484
+ const json = await response.json();
485
+ return json.data.children.filter((child) => child.kind === "t3").map((child) => {
486
+ const post = child.data;
487
+ return {
488
+ id: post.id,
489
+ title: post.title,
490
+ author: post.author,
491
+ subreddit: post.subreddit,
492
+ selftext: post.selftext || "",
493
+ url: post.url,
494
+ score: post.score,
495
+ upvoteRatio: post.upvote_ratio,
496
+ numComments: post.num_comments,
497
+ createdUtc: post.created_utc,
498
+ over18: post.over_18,
499
+ spoiler: post.spoiler,
500
+ edited: !!post.edited,
501
+ isSelf: post.is_self,
502
+ linkFlairText: post.link_flair_text ?? void 0,
503
+ permalink: post.permalink
504
+ };
505
+ });
506
+ } catch {
507
+ throw new Error(`Failed to get posts for user ${username}`);
508
+ }
509
+ }
510
+ async getUserComments(username, options = {}) {
511
+ await this.authenticate();
512
+ try {
513
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
514
+ const params = new URLSearchParams({
515
+ sort,
516
+ t: timeFilter,
517
+ limit: limit.toString()
518
+ });
519
+ const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
520
+ if (!response.ok) {
521
+ throw new Error(`HTTP ${response.status}`);
522
+ }
523
+ const json = await response.json();
524
+ return json.data.children.filter((child) => child.kind === "t1").map((child) => {
525
+ const comment = child.data;
526
+ return {
527
+ id: comment.id,
528
+ author: comment.author,
529
+ body: comment.body,
530
+ score: comment.score,
531
+ controversiality: comment.controversiality,
532
+ subreddit: comment.subreddit,
533
+ submissionTitle: comment.link_title || "",
534
+ createdUtc: comment.created_utc,
535
+ edited: !!comment.edited,
536
+ isSubmitter: comment.is_submitter,
537
+ permalink: comment.permalink
538
+ };
539
+ });
540
+ } catch {
541
+ throw new Error(`Failed to get comments for user ${username}`);
542
+ }
543
+ }
544
+ };
545
+ var redditClient = null;
546
+ function initializeRedditClient(config) {
547
+ redditClient = new RedditClient(config);
548
+ return redditClient;
549
+ }
550
+ function getRedditClient() {
551
+ return redditClient;
552
+ }
553
+
554
+ // src/utils/formatters.ts
555
+ function formatTimestamp(timestamp) {
556
+ try {
557
+ const date = new Date(timestamp * 1e3);
558
+ return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
559
+ } catch {
560
+ return String(timestamp);
561
+ }
562
+ }
563
+ function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
564
+ const insights = [];
565
+ if (karmaRatio > 5) {
566
+ insights.push("Primarily a commenter, highly engaged in discussions");
567
+ } else if (karmaRatio < 0.2) {
568
+ insights.push("Content creator, focuses on sharing posts");
569
+ } else {
570
+ insights.push("Balanced participation in both posting and commenting");
571
+ }
572
+ if (accountAgeDays < 30) {
573
+ insights.push("New user, still exploring Reddit");
574
+ } else if (accountAgeDays > 365 * 5) {
575
+ insights.push("Long-time Redditor with extensive platform experience");
576
+ }
577
+ if (isMod) {
578
+ insights.push("Community leader who helps maintain subreddit quality");
579
+ }
580
+ return insights.join("\n - ");
581
+ }
582
+ function analyzePostEngagement(score, ratio, numComments) {
583
+ const insights = [];
584
+ if (score > 1e3 && ratio > 0.95) {
585
+ insights.push("Highly successful post with strong community approval");
586
+ } else if (score > 100 && ratio > 0.8) {
587
+ insights.push("Well-received post with good engagement");
588
+ } else if (ratio < 0.5) {
589
+ insights.push("Controversial post that sparked debate");
590
+ }
591
+ if (numComments > 100) {
592
+ insights.push("Generated significant discussion");
593
+ } else if (numComments > score * 0.5) {
594
+ insights.push("Highly discussable content with active comment section");
595
+ } else if (numComments === 0) {
596
+ insights.push("Yet to receive community interaction");
597
+ }
598
+ return insights.join("\n - ");
599
+ }
600
+ function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
601
+ const insights = [];
602
+ if (subscribers > 1e6) {
603
+ insights.push("Major subreddit with massive following");
604
+ } else if (subscribers > 1e5) {
605
+ insights.push("Well-established community");
606
+ } else if (subscribers < 1e3) {
607
+ insights.push("Niche community, potential for growth");
608
+ }
609
+ if (activeUsers) {
610
+ const activityRatio = activeUsers / subscribers;
611
+ if (activityRatio > 0.1) {
612
+ insights.push("Highly active community with strong engagement");
613
+ } else if (activityRatio < 0.01) {
614
+ insights.push("Could benefit from more community engagement initiatives");
615
+ }
616
+ }
617
+ if (ageDays > 365 * 5) {
618
+ insights.push("Mature subreddit with established culture");
619
+ } else if (ageDays < 90) {
620
+ insights.push("New subreddit still forming its community");
621
+ }
622
+ return insights.join("\n - ");
623
+ }
624
+ function getUserRecommendations(karmaRatio, isMod, accountAgeDays) {
625
+ const recommendations = [];
626
+ if (karmaRatio > 5) {
627
+ recommendations.push("Consider creating more posts to share your expertise");
628
+ } else if (karmaRatio < 0.2) {
629
+ recommendations.push("Engage more in discussions to build community connections");
630
+ }
631
+ if (accountAgeDays < 30) {
632
+ recommendations.push("Explore popular subreddits in your areas of interest");
633
+ recommendations.push("Read community guidelines before posting");
634
+ }
635
+ if (isMod) {
636
+ recommendations.push("Share moderation insights with other community leaders");
637
+ }
638
+ if (!recommendations.length) {
639
+ recommendations.push("Maintain your balanced engagement across Reddit");
640
+ }
641
+ return recommendations.join("\n - ");
642
+ }
643
+ function getBestEngagementTime(createdUtc) {
644
+ const postHour = new Date(createdUtc * 1e3).getHours();
645
+ if (14 <= postHour && postHour <= 18) {
646
+ return "Posted during peak engagement hours (2 PM - 6 PM), good timing!";
647
+ } else if (23 <= postHour || postHour <= 5) {
648
+ return "Consider posting during more active hours (morning to evening)";
649
+ } else {
650
+ return "Posted during moderate activity hours, timing could be optimized";
651
+ }
652
+ }
653
+ function getSubredditEngagementTips(subreddit) {
654
+ const tips = [];
655
+ if (subreddit.subscribers > 1e6) {
656
+ tips.push("Post during peak hours for maximum visibility");
657
+ tips.push("Ensure content is highly polished due to high competition");
658
+ } else if (subreddit.subscribers < 1e3) {
659
+ tips.push("Engage actively to help grow the community");
660
+ tips.push("Consider cross-posting to related larger subreddits");
661
+ }
662
+ if (subreddit.activeUserCount) {
663
+ const activityRatio = subreddit.activeUserCount / subreddit.subscribers;
664
+ if (activityRatio > 0.1) {
665
+ tips.push("Quick responses recommended due to high activity");
666
+ }
667
+ }
668
+ return tips.length ? tips.join("\n - ") : "Regular engagement recommended to maintain community presence";
669
+ }
670
+ function analyzeCommentImpact(score, isEdited, isOp) {
671
+ const insights = [];
672
+ if (score > 100) {
673
+ insights.push("Highly upvoted comment with significant community agreement");
674
+ } else if (score < 0) {
675
+ insights.push("Controversial or contested viewpoint");
676
+ }
677
+ if (isEdited) {
678
+ insights.push("Refined for clarity or accuracy");
679
+ }
680
+ if (isOp) {
681
+ insights.push("Author's perspective adds context to original post");
682
+ }
683
+ return insights.length ? insights.join("\n - ") : "Standard engagement with discussion";
684
+ }
685
+ function formatUserInfo(user) {
686
+ const status = [];
687
+ if (user.isMod) status.push("Moderator");
688
+ if (user.isGold) status.push("Reddit Gold Member");
689
+ if (user.isEmployee) status.push("Reddit Employee");
690
+ const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / (24 * 3600);
691
+ const karmaRatio = user.commentKarma / (user.linkKarma || 1);
692
+ return {
693
+ username: user.name,
694
+ karma: {
695
+ commentKarma: user.commentKarma,
696
+ postKarma: user.linkKarma,
697
+ totalKarma: user.totalKarma
698
+ },
699
+ accountStatus: status.length ? status : ["Regular User"],
700
+ accountCreated: formatTimestamp(user.createdUtc),
701
+ profileUrl: user.profileUrl,
702
+ activityAnalysis: analyzeUserActivity(karmaRatio, user.isMod, accountAgeDays),
703
+ recommendations: getUserRecommendations(karmaRatio, user.isMod, accountAgeDays)
704
+ };
705
+ }
706
+ function formatPostInfo(post) {
707
+ const contentType = post.isSelf ? "Text Post" : "Link Post";
708
+ const content = post.isSelf ? post.selftext || "" : post.url || "";
709
+ const flags = [];
710
+ if (post.over18) flags.push("NSFW");
711
+ if (post.spoiler) flags.push("Spoiler");
712
+ if (post.edited) flags.push("Edited");
713
+ return {
714
+ title: post.title,
715
+ type: contentType,
716
+ content: content.length > 300 ? content.substring(0, 297) + "..." : content,
717
+ author: post.author,
718
+ subreddit: post.subreddit,
719
+ stats: {
720
+ score: post.score,
721
+ upvoteRatio: post.upvoteRatio,
722
+ comments: post.numComments
723
+ },
724
+ metadata: {
725
+ posted: formatTimestamp(post.createdUtc),
726
+ flags,
727
+ flair: post.linkFlairText || "None"
728
+ },
729
+ links: {
730
+ fullPost: `https://reddit.com${post.permalink}`,
731
+ shortLink: `https://redd.it/${post.id}`
732
+ },
733
+ engagementAnalysis: analyzePostEngagement(post.score, post.upvoteRatio, post.numComments),
734
+ bestTimeToEngage: getBestEngagementTime(post.createdUtc)
735
+ };
736
+ }
737
+ function formatSubredditInfo(subreddit) {
738
+ const flags = [];
739
+ if (subreddit.over18) flags.push("NSFW");
740
+ if (subreddit.subredditType) flags.push(`Type: ${subreddit.subredditType}`);
741
+ const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / (24 * 3600);
742
+ return {
743
+ name: subreddit.displayName,
744
+ title: subreddit.title,
745
+ stats: {
746
+ subscribers: subreddit.subscribers,
747
+ activeUsers: subreddit.activeUserCount !== void 0 ? subreddit.activeUserCount : "Unknown"
748
+ },
749
+ description: {
750
+ short: subreddit.publicDescription,
751
+ full: subreddit.description.length > 300 ? subreddit.description.substring(0, 297) + "..." : subreddit.description
752
+ },
753
+ metadata: {
754
+ created: formatTimestamp(subreddit.createdUtc),
755
+ flags: flags.length ? flags : ["None"]
756
+ },
757
+ links: {
758
+ subreddit: `https://reddit.com${subreddit.url}`,
759
+ wiki: `https://reddit.com/r/${subreddit.displayName}/wiki`
760
+ },
761
+ communityAnalysis: analyzeSubredditHealth(subreddit.subscribers, subreddit.activeUserCount, ageDays),
762
+ engagementTips: getSubredditEngagementTips(subreddit)
763
+ };
764
+ }
765
+ function formatCommentInfo(comment) {
766
+ const flags = [];
767
+ if (comment.edited) flags.push("Edited");
768
+ if (comment.isSubmitter) flags.push("OP");
769
+ return {
770
+ author: comment.author,
771
+ content: comment.body.length > 300 ? comment.body.substring(0, 297) + "..." : comment.body,
772
+ stats: {
773
+ score: comment.score,
774
+ controversiality: comment.controversiality
775
+ },
776
+ context: {
777
+ subreddit: comment.subreddit,
778
+ thread: comment.submissionTitle
779
+ },
780
+ metadata: {
781
+ posted: formatTimestamp(comment.createdUtc),
782
+ flags: flags.length ? flags : ["None"]
783
+ },
784
+ link: `https://reddit.com${comment.permalink}`,
785
+ commentAnalysis: analyzeCommentImpact(comment.score, comment.edited, comment.isSubmitter)
786
+ };
787
+ }
788
+ function formatPost(post) {
789
+ return {
790
+ title: post.title,
791
+ author: post.author,
792
+ subreddit: post.subreddit,
793
+ score: post.score,
794
+ upvoteRatio: Math.round(post.upvoteRatio * 100),
795
+ numComments: post.numComments,
796
+ createdAt: formatTimestamp(post.createdUtc),
797
+ selftext: post.selftext,
798
+ permalink: post.permalink,
799
+ nsfw: post.over18,
800
+ spoiler: post.spoiler
801
+ };
802
+ }
803
+
804
+ // src/tools/user-tools.ts
805
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
806
+ async function getUserInfo(params) {
807
+ const { username } = params;
808
+ const client = getRedditClient();
809
+ if (!client) {
810
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
811
+ }
812
+ try {
813
+ const user = await client.getUser(username);
814
+ const formattedUser = formatUserInfo(user);
815
+ return {
816
+ content: [
817
+ {
818
+ type: "text",
819
+ text: `
820
+ # User Information: u/${formattedUser.username}
821
+
822
+ ## Profile Overview
823
+ - Username: u/${formattedUser.username}
824
+ - Karma:
825
+ - Comment Karma: ${formattedUser.karma.commentKarma.toLocaleString()}
826
+ - Post Karma: ${formattedUser.karma.postKarma.toLocaleString()}
827
+ - Total Karma: ${formattedUser.karma.totalKarma.toLocaleString()}
828
+ - Account Status: ${formattedUser.accountStatus.join(", ")}
829
+ - Account Created: ${formattedUser.accountCreated}
830
+ - Profile URL: ${formattedUser.profileUrl}
831
+
832
+ ## Activity Analysis
833
+ - ${formattedUser.activityAnalysis.replace(/\n - /g, "\n- ")}
834
+
835
+ ## Recommendations
836
+ - ${formattedUser.recommendations.replace(/\n - /g, "\n- ")}
837
+ `
838
+ }
839
+ ]
840
+ };
841
+ } catch (error) {
842
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user data: ${String(error)}`);
843
+ }
844
+ }
845
+ async function getUserPosts(params) {
846
+ const { username, sort = "new", time_filter = "all", limit = 10 } = params;
847
+ const client = getRedditClient();
848
+ if (!client) {
849
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
850
+ }
851
+ try {
852
+ const posts = await client.getUserPosts(username, {
853
+ sort,
854
+ timeFilter: time_filter,
855
+ limit
856
+ });
857
+ return {
858
+ content: [
859
+ {
860
+ type: "text",
861
+ text: `# Posts by u/${username}
862
+
863
+ ## Sort: ${sort} | Time: ${time_filter} | Count: ${posts.length}
864
+
865
+ ${posts.map((post, index) => {
866
+ const date = new Date(post.createdUtc * 1e3).toLocaleString();
867
+ const selftext = post.selftext ? `
868
+ ${post.selftext.substring(0, 200)}${post.selftext.length > 200 ? "..." : ""}
869
+ ` : "";
870
+ return `### ${index + 1}. ${post.title}
871
+ - Subreddit: r/${post.subreddit}
872
+ - Score: ${post.score} (${Math.round(post.upvoteRatio * 100)}% upvoted)
873
+ - Comments: ${post.numComments}
874
+ - Posted: ${date}
875
+ ${selftext}
876
+ - Link: https://reddit.com${post.permalink}
877
+ ${post.over18 ? "- **NSFW**" : ""}
878
+ ${post.spoiler ? "- **Spoiler**" : ""}`;
879
+ }).join("\n\n---\n\n")}`
880
+ }
881
+ ]
882
+ };
883
+ } catch (error) {
884
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user posts: ${String(error)}`);
885
+ }
886
+ }
887
+ async function getUserComments(params) {
888
+ const { username, sort = "new", time_filter = "all", limit = 10 } = params;
889
+ const client = getRedditClient();
890
+ if (!client) {
891
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, "Reddit client not initialized");
892
+ }
893
+ try {
894
+ const comments = await client.getUserComments(username, {
895
+ sort,
896
+ timeFilter: time_filter,
897
+ limit
898
+ });
899
+ return {
900
+ content: [
901
+ {
902
+ type: "text",
903
+ text: `# Comments by u/${username}
904
+
905
+ ## Sort: ${sort} | Time: ${time_filter} | Count: ${comments.length}
906
+
907
+ ${comments.map((comment, index) => {
908
+ const date = new Date(comment.createdUtc * 1e3).toLocaleString();
909
+ const edited = comment.edited ? " *(edited)*" : "";
910
+ const body = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body;
911
+ return `### ${index + 1}. In r/${comment.subreddit} on "${comment.submissionTitle}"
912
+ - Score: ${comment.score} points
913
+ - Posted: ${date}${edited}
914
+ - Link: https://reddit.com${comment.permalink}
915
+
916
+ ${body}`;
917
+ }).join("\n\n---\n\n")}`
918
+ }
919
+ ]
920
+ };
921
+ } catch (error) {
922
+ throw new import_types.McpError(import_types.ErrorCode.InternalError, `Failed to fetch user comments: ${String(error)}`);
923
+ }
924
+ }
925
+
926
+ // src/tools/post-tools.ts
927
+ var import_types2 = require("@modelcontextprotocol/sdk/types.js");
928
+ async function getRedditPost(params) {
929
+ const { subreddit, post_id } = params;
930
+ const client = getRedditClient();
931
+ if (!client) {
932
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, "Reddit client not initialized");
933
+ }
934
+ try {
935
+ const post = await client.getPost(post_id, subreddit);
936
+ const formattedPost = formatPostInfo(post);
937
+ return {
938
+ content: [
939
+ {
940
+ type: "text",
941
+ text: `
942
+ # Post from r/${formattedPost.subreddit}
943
+
944
+ ## Post Details
945
+ - Title: ${formattedPost.title}
946
+ - Type: ${formattedPost.type}
947
+ - Author: u/${formattedPost.author}
948
+
949
+ ## Content
950
+ ${formattedPost.content}
951
+
952
+ ## Stats
953
+ - Score: ${formattedPost.stats.score.toLocaleString()}
954
+ - Upvote Ratio: ${(formattedPost.stats.upvoteRatio * 100).toFixed(1)}%
955
+ - Comments: ${formattedPost.stats.comments.toLocaleString()}
956
+
957
+ ## Metadata
958
+ - Posted: ${formattedPost.metadata.posted}
959
+ - Flags: ${formattedPost.metadata.flags.length ? formattedPost.metadata.flags.join(", ") : "None"}
960
+ - Flair: ${formattedPost.metadata.flair}
961
+
962
+ ## Links
963
+ - Full Post: ${formattedPost.links.fullPost}
964
+ - Short Link: ${formattedPost.links.shortLink}
965
+
966
+ ## Engagement Analysis
967
+ - ${formattedPost.engagementAnalysis.replace(/\n - /g, "\n- ")}
968
+
969
+ ## Best Time to Engage
970
+ ${formattedPost.bestTimeToEngage}
971
+ `
972
+ }
973
+ ]
974
+ };
975
+ } catch (error) {
976
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, `Failed to fetch post data: ${String(error)}`);
977
+ }
978
+ }
979
+ async function getTopPosts(params) {
980
+ const { subreddit, time_filter = "week", limit = 10 } = params;
981
+ const client = getRedditClient();
982
+ if (!client) {
983
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, "Reddit client not initialized");
984
+ }
985
+ try {
986
+ const posts = await client.getTopPosts(subreddit, time_filter, limit);
987
+ const formattedPosts = posts.map(formatPostInfo);
988
+ const postSummaries = formattedPosts.map(
989
+ (post, index) => `
990
+ ### ${index + 1}. ${post.title}
991
+ - Author: u/${post.author}
992
+ - Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
993
+ - Comments: ${post.stats.comments.toLocaleString()}
994
+ - Posted: ${post.metadata.posted}
995
+ - Link: ${post.links.shortLink}
996
+ `
997
+ ).join("\n");
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: `
1003
+ # Top Posts from r/${subreddit} (${time_filter})
1004
+
1005
+ ${postSummaries}
1006
+ `
1007
+ }
1008
+ ]
1009
+ };
1010
+ } catch (error) {
1011
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, `Failed to fetch top posts: ${String(error)}`);
1012
+ }
1013
+ }
1014
+ async function createPost(params) {
1015
+ const { subreddit, title, content, is_self = true } = params;
1016
+ const client = getRedditClient();
1017
+ if (!client) {
1018
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, "Reddit client not initialized");
1019
+ }
1020
+ try {
1021
+ const post = await client.createPost(subreddit, title, content, is_self);
1022
+ const formattedPost = formatPostInfo(post);
1023
+ return {
1024
+ content: [
1025
+ {
1026
+ type: "text",
1027
+ text: `
1028
+ # Post Created Successfully
1029
+
1030
+ ## Post Details
1031
+ - Title: ${formattedPost.title}
1032
+ - Subreddit: r/${formattedPost.subreddit}
1033
+ - Type: ${formattedPost.type}
1034
+ - Link: ${formattedPost.links.fullPost}
1035
+
1036
+ Your post has been successfully submitted to r/${formattedPost.subreddit}.
1037
+ `
1038
+ }
1039
+ ]
1040
+ };
1041
+ } catch (error) {
1042
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, `Failed to create post: ${String(error)}`);
1043
+ }
1044
+ }
1045
+ async function replyToPost(params) {
1046
+ const { post_id, content } = params;
1047
+ const client = getRedditClient();
1048
+ if (!client) {
1049
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, "Reddit client not initialized");
1050
+ }
1051
+ try {
1052
+ const comment = await client.replyToPost(post_id, content);
1053
+ const formattedComment = formatCommentInfo(comment);
1054
+ return {
1055
+ content: [
1056
+ {
1057
+ type: "text",
1058
+ text: `
1059
+ # Reply Posted Successfully
1060
+
1061
+ ## Comment Details
1062
+ - Author: u/${formattedComment.author}
1063
+ - Subreddit: r/${formattedComment.context.subreddit}
1064
+ - Thread: ${formattedComment.context.thread}
1065
+ - Link: ${formattedComment.link}
1066
+
1067
+ Your reply has been successfully posted.
1068
+ `
1069
+ }
1070
+ ]
1071
+ };
1072
+ } catch (error) {
1073
+ throw new import_types2.McpError(import_types2.ErrorCode.InternalError, `Failed to reply to post: ${String(error)}`);
1074
+ }
1075
+ }
1076
+
1077
+ // src/tools/subreddit-tools.ts
1078
+ var import_types3 = require("@modelcontextprotocol/sdk/types.js");
1079
+ async function getSubredditInfo(params) {
1080
+ const { subreddit_name } = params;
1081
+ const client = getRedditClient();
1082
+ if (!client) {
1083
+ throw new import_types3.McpError(import_types3.ErrorCode.InternalError, "Reddit client not initialized");
1084
+ }
1085
+ try {
1086
+ const subreddit = await client.getSubredditInfo(subreddit_name);
1087
+ const formattedSubreddit = formatSubredditInfo(subreddit);
1088
+ return {
1089
+ content: [
1090
+ {
1091
+ type: "text",
1092
+ text: `
1093
+ # Subreddit Information: r/${formattedSubreddit.name}
1094
+
1095
+ ## Overview
1096
+ - Name: r/${formattedSubreddit.name}
1097
+ - Title: ${formattedSubreddit.title}
1098
+ - Subscribers: ${formattedSubreddit.stats.subscribers.toLocaleString()}
1099
+ - Active Users: ${typeof formattedSubreddit.stats.activeUsers === "number" ? formattedSubreddit.stats.activeUsers.toLocaleString() : formattedSubreddit.stats.activeUsers}
1100
+
1101
+ ## Description
1102
+ ${formattedSubreddit.description.short}
1103
+
1104
+ ## Detailed Description
1105
+ ${formattedSubreddit.description.full}
1106
+
1107
+ ## Metadata
1108
+ - Created: ${formattedSubreddit.metadata.created}
1109
+ - Flags: ${formattedSubreddit.metadata.flags.join(", ")}
1110
+
1111
+ ## Links
1112
+ - Subreddit: ${formattedSubreddit.links.subreddit}
1113
+ - Wiki: ${formattedSubreddit.links.wiki}
1114
+
1115
+ ## Community Analysis
1116
+ - ${formattedSubreddit.communityAnalysis.replace(/\n - /g, "\n- ")}
1117
+
1118
+ ## Engagement Tips
1119
+ - ${formattedSubreddit.engagementTips.replace(/\n - /g, "\n- ")}
1120
+ `
1121
+ }
1122
+ ]
1123
+ };
1124
+ } catch (error) {
1125
+ throw new import_types3.McpError(import_types3.ErrorCode.InternalError, `Failed to fetch subreddit data: ${String(error)}`);
1126
+ }
1127
+ }
1128
+ async function getTrendingSubreddits() {
1129
+ const client = getRedditClient();
1130
+ if (!client) {
1131
+ throw new import_types3.McpError(import_types3.ErrorCode.InternalError, "Reddit client not initialized");
1132
+ }
1133
+ try {
1134
+ const trendingSubreddits = await client.getTrendingSubreddits();
1135
+ return {
1136
+ content: [
1137
+ {
1138
+ type: "text",
1139
+ text: `
1140
+ # Trending Subreddits
1141
+
1142
+ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}
1143
+ `
1144
+ }
1145
+ ]
1146
+ };
1147
+ } catch (error) {
1148
+ throw new import_types3.McpError(import_types3.ErrorCode.InternalError, `Failed to fetch trending subreddits: ${String(error)}`);
1149
+ }
1150
+ }
1151
+
1152
+ // src/tools/search-tools.ts
1153
+ var import_types4 = require("@modelcontextprotocol/sdk/types.js");
1154
+ async function searchReddit(params) {
1155
+ const { query, subreddit, sort = "relevance", time_filter = "all", limit = 10, type = "link" } = params;
1156
+ const client = getRedditClient();
1157
+ if (!client) {
1158
+ throw new import_types4.McpError(import_types4.ErrorCode.InternalError, "Reddit client not initialized");
1159
+ }
1160
+ if (!query || query.trim().length === 0) {
1161
+ throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, "Search query cannot be empty");
1162
+ }
1163
+ try {
1164
+ const posts = await client.searchReddit(query, {
1165
+ subreddit,
1166
+ sort,
1167
+ timeFilter: time_filter,
1168
+ limit,
1169
+ type
1170
+ });
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: "text",
1175
+ text: `# Reddit Search Results for: "${query}"${subreddit ? ` in r/${subreddit}` : ""}
1176
+
1177
+ ## Search Parameters
1178
+ - Sort: ${sort}
1179
+ - Time Filter: ${time_filter}
1180
+ - Type: ${type}
1181
+ - Results: ${posts.length}
1182
+
1183
+ ${posts.map((post, index) => {
1184
+ const formatted = formatPost(post);
1185
+ return `### ${index + 1}. ${formatted.title}
1186
+ - Author: u/${formatted.author}
1187
+ - Subreddit: r/${formatted.subreddit}
1188
+ - Score: ${formatted.score} (${formatted.upvoteRatio}% upvoted)
1189
+ - Comments: ${formatted.numComments}
1190
+ - Posted: ${formatted.createdAt}
1191
+ ${formatted.selftext ? `
1192
+ ${formatted.selftext.substring(0, 200)}${formatted.selftext.length > 200 ? "..." : ""}
1193
+ ` : ""}
1194
+ - Link: https://reddit.com${formatted.permalink}
1195
+ ${formatted.nsfw ? "- **NSFW**" : ""}
1196
+ ${formatted.spoiler ? "- **Spoiler**" : ""}
1197
+ `;
1198
+ }).join("\n")}`
1199
+ }
1200
+ ]
1201
+ };
1202
+ } catch (error) {
1203
+ throw new import_types4.McpError(import_types4.ErrorCode.InternalError, `Failed to search Reddit: ${String(error)}`);
1204
+ }
1205
+ }
1206
+
1207
+ // src/tools/comment-tools.ts
1208
+ var import_types5 = require("@modelcontextprotocol/sdk/types.js");
1209
+ async function getPostComments(params) {
1210
+ const { post_id, subreddit, sort = "best", limit = 100 } = params;
1211
+ const client = getRedditClient();
1212
+ if (!client) {
1213
+ throw new import_types5.McpError(import_types5.ErrorCode.InternalError, "Reddit client not initialized");
1214
+ }
1215
+ if (!post_id || !subreddit) {
1216
+ throw new import_types5.McpError(import_types5.ErrorCode.InvalidParams, "post_id and subreddit are required");
1217
+ }
1218
+ try {
1219
+ const { post, comments } = await client.getPostComments(post_id, subreddit, {
1220
+ sort,
1221
+ limit
1222
+ });
1223
+ const formattedPost = formatPost(post);
1224
+ const formatComment = (comment) => {
1225
+ const edited = comment.edited ? " *(edited)*" : "";
1226
+ const submitter = comment.isSubmitter ? " **[OP]**" : "";
1227
+ const depth = comment.depth || 0;
1228
+ const prefix = " ".repeat(depth) + (depth > 0 ? "\u2514\u2500 " : "");
1229
+ return `${prefix}**u/${comment.author}**${submitter} \u2022 ${comment.score} points \u2022 ${new Date(comment.createdUtc * 1e3).toLocaleString()}${edited}
1230
+ ${prefix}${comment.body.split("\n").join(`
1231
+ ${prefix}`)}`;
1232
+ };
1233
+ return {
1234
+ content: [
1235
+ {
1236
+ type: "text",
1237
+ text: `# Comments for: ${formattedPost.title}
1238
+
1239
+ ## Post Details
1240
+ - Author: u/${formattedPost.author}
1241
+ - Subreddit: r/${formattedPost.subreddit}
1242
+ - Score: ${formattedPost.score} (${formattedPost.upvoteRatio}% upvoted)
1243
+ - Posted: ${formattedPost.createdAt}
1244
+ - Link: https://reddit.com${formattedPost.permalink}
1245
+
1246
+ ## Post Content
1247
+ ${formattedPost.selftext || "[Link post - no text content]"}
1248
+
1249
+ ## Comments (${comments.length} loaded, sorted by ${sort})
1250
+
1251
+ ${comments.map((comment) => formatComment(comment)).join("\n\n---\n\n")}`
1252
+ }
1253
+ ]
1254
+ };
1255
+ } catch (error) {
1256
+ throw new import_types5.McpError(import_types5.ErrorCode.InternalError, `Failed to fetch comments: ${String(error)}`);
1257
+ }
1258
+ }
1259
+
1260
+ // src/middleware/auth.ts
1261
+ var import_http_exception = require("hono/http-exception");
1262
+ function createAuthMiddleware(config = {}) {
1263
+ return async (c, next) => {
1264
+ if (!config.enabled || !config.token) {
1265
+ return next();
1266
+ }
1267
+ const authHeader = c.req.header("Authorization");
1268
+ if (!authHeader) {
1269
+ throw new import_http_exception.HTTPException(401, {
1270
+ message: "Authorization header required"
1271
+ });
1272
+ }
1273
+ const [scheme, token] = authHeader.split(" ");
1274
+ if (scheme !== "Bearer") {
1275
+ throw new import_http_exception.HTTPException(401, {
1276
+ message: "Invalid authorization scheme. Use 'Bearer <token>'"
1277
+ });
1278
+ }
1279
+ if (!token) {
1280
+ throw new import_http_exception.HTTPException(401, {
1281
+ message: "Bearer token required"
1282
+ });
1283
+ }
1284
+ if (token !== config.token) {
1285
+ throw new import_http_exception.HTTPException(403, {
1286
+ message: "Invalid token"
1287
+ });
1288
+ }
1289
+ return next();
1290
+ };
1291
+ }
1292
+ function generateRandomToken(length = 32) {
1293
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1294
+ let result = "";
1295
+ for (let i = 0; i < length; i++) {
1296
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1297
+ }
1298
+ return result;
1299
+ }
1300
+
1301
+ // src/server.ts
1302
+ var import_dotenv = __toESM(require("dotenv"));
1303
+ import_dotenv.default.config();
1304
+ var app = new import_hono.Hono();
1305
+ var authToken = process.env.OAUTH_TOKEN || (process.env.OAUTH_ENABLED === "true" ? generateRandomToken() : void 0);
1306
+ var authConfig = {
1307
+ enabled: process.env.OAUTH_ENABLED === "true",
1308
+ token: authToken
1309
+ };
1310
+ if (authConfig.enabled) {
1311
+ if (process.env.OAUTH_TOKEN) {
1312
+ console.log("[Auth] OAuth enabled with provided token");
1313
+ } else {
1314
+ console.log(`[Auth] OAuth enabled with generated token: ${authToken}`);
1315
+ }
1316
+ } else {
1317
+ console.log("[Auth] OAuth disabled - server accessible without authentication");
1318
+ }
1319
+ app.use(
1320
+ "*",
1321
+ (0, import_cors.cors)({
1322
+ origin: "*",
1323
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
1324
+ allowHeaders: ["*"]
1325
+ })
1326
+ );
1327
+ app.use("/mcp", createAuthMiddleware(authConfig));
1328
+ app.get("/", (c) => {
1329
+ return c.json({
1330
+ status: "ok",
1331
+ service: "Reddit MCP Server",
1332
+ endpoint: "/mcp",
1333
+ version: "1.0.10",
1334
+ auth: {
1335
+ enabled: authConfig.enabled,
1336
+ ...authConfig.enabled && { token_provided: !!process.env.OAUTH_TOKEN }
1337
+ }
1338
+ });
1339
+ });
1340
+ function createMCPServer() {
1341
+ const server = new import_server.Server(
1342
+ {
1343
+ name: "reddit-mcp-server",
1344
+ version: "1.0.10"
1345
+ },
1346
+ {
1347
+ capabilities: {
1348
+ tools: {}
1349
+ }
1350
+ }
1351
+ );
1352
+ server.setRequestHandler(import_types6.ListToolsRequestSchema, async () => ({
1353
+ tools: [
1354
+ {
1355
+ name: "test_reddit_mcp_server",
1356
+ description: "Test the Reddit MCP Server",
1357
+ inputSchema: {
1358
+ type: "object",
1359
+ properties: {}
1360
+ }
1361
+ },
1362
+ {
1363
+ name: "get_reddit_post",
1364
+ description: "Get a Reddit post",
1365
+ inputSchema: {
1366
+ type: "object",
1367
+ properties: {
1368
+ subreddit: {
1369
+ type: "string",
1370
+ description: "The subreddit to fetch posts from"
1371
+ },
1372
+ post_id: {
1373
+ type: "string",
1374
+ description: "The ID of the post to fetch"
1375
+ }
1376
+ },
1377
+ required: ["subreddit", "post_id"]
1378
+ }
1379
+ },
1380
+ {
1381
+ name: "get_top_posts",
1382
+ description: "Get top posts from a subreddit",
1383
+ inputSchema: {
1384
+ type: "object",
1385
+ properties: {
1386
+ subreddit: {
1387
+ type: "string",
1388
+ description: "Name of the subreddit"
1389
+ },
1390
+ time_filter: {
1391
+ type: "string",
1392
+ description: "Time period to filter posts (e.g. 'day', 'week', 'month', 'year', 'all')",
1393
+ enum: ["day", "week", "month", "year", "all"],
1394
+ default: "week"
1395
+ },
1396
+ limit: {
1397
+ type: "integer",
1398
+ description: "Number of posts to fetch",
1399
+ default: 10
1400
+ }
1401
+ },
1402
+ required: ["subreddit"]
1403
+ }
1404
+ },
1405
+ {
1406
+ name: "get_user_info",
1407
+ description: "Get information about a Reddit user",
1408
+ inputSchema: {
1409
+ type: "object",
1410
+ properties: {
1411
+ username: {
1412
+ type: "string",
1413
+ description: "The username of the Reddit user to get info for"
1414
+ }
1415
+ },
1416
+ required: ["username"]
1417
+ }
1418
+ },
1419
+ {
1420
+ name: "get_subreddit_info",
1421
+ description: "Get information about a subreddit",
1422
+ inputSchema: {
1423
+ type: "object",
1424
+ properties: {
1425
+ subreddit_name: {
1426
+ type: "string",
1427
+ description: "Name of the subreddit"
1428
+ }
1429
+ },
1430
+ required: ["subreddit_name"]
1431
+ }
1432
+ },
1433
+ {
1434
+ name: "get_trending_subreddits",
1435
+ description: "Get currently trending subreddits",
1436
+ inputSchema: {
1437
+ type: "object",
1438
+ properties: {}
1439
+ }
1440
+ },
1441
+ {
1442
+ name: "create_post",
1443
+ description: "Create a new post in a subreddit",
1444
+ inputSchema: {
1445
+ type: "object",
1446
+ properties: {
1447
+ subreddit: {
1448
+ type: "string",
1449
+ description: "Name of the subreddit to post in"
1450
+ },
1451
+ title: {
1452
+ type: "string",
1453
+ description: "Title of the post"
1454
+ },
1455
+ content: {
1456
+ type: "string",
1457
+ description: "Content of the post (text for self posts, URL for link posts)"
1458
+ },
1459
+ is_self: {
1460
+ type: "boolean",
1461
+ description: "Whether this is a self (text) post (true) or link post (false)",
1462
+ default: true
1463
+ }
1464
+ },
1465
+ required: ["subreddit", "title", "content"]
1466
+ }
1467
+ },
1468
+ {
1469
+ name: "reply_to_post",
1470
+ description: "Post a reply to an existing Reddit post",
1471
+ inputSchema: {
1472
+ type: "object",
1473
+ properties: {
1474
+ post_id: {
1475
+ type: "string",
1476
+ description: "The ID of the post to reply to"
1477
+ },
1478
+ content: {
1479
+ type: "string",
1480
+ description: "The content of the reply"
1481
+ },
1482
+ subreddit: {
1483
+ type: "string",
1484
+ description: "The subreddit name if known (for validation)"
1485
+ }
1486
+ },
1487
+ required: ["post_id", "content"]
1488
+ }
1489
+ },
1490
+ {
1491
+ name: "search_reddit",
1492
+ description: "Search for posts on Reddit",
1493
+ inputSchema: {
1494
+ type: "object",
1495
+ properties: {
1496
+ query: {
1497
+ type: "string",
1498
+ description: "The search query"
1499
+ },
1500
+ subreddit: {
1501
+ type: "string",
1502
+ description: "Search within a specific subreddit (optional)"
1503
+ },
1504
+ sort: {
1505
+ type: "string",
1506
+ description: "Sort order: relevance, hot, top, new, comments",
1507
+ enum: ["relevance", "hot", "top", "new", "comments"],
1508
+ default: "relevance"
1509
+ },
1510
+ time_filter: {
1511
+ type: "string",
1512
+ description: "Time filter: hour, day, week, month, year, all",
1513
+ enum: ["hour", "day", "week", "month", "year", "all"],
1514
+ default: "all"
1515
+ },
1516
+ limit: {
1517
+ type: "number",
1518
+ description: "Maximum number of results to return",
1519
+ minimum: 1,
1520
+ maximum: 100,
1521
+ default: 10
1522
+ },
1523
+ type: {
1524
+ type: "string",
1525
+ description: "Type of content: link (posts), sr (subreddits), user (users)",
1526
+ enum: ["link", "sr", "user"],
1527
+ default: "link"
1528
+ }
1529
+ },
1530
+ required: ["query"]
1531
+ }
1532
+ },
1533
+ {
1534
+ name: "get_post_comments",
1535
+ description: "Get comments for a specific Reddit post",
1536
+ inputSchema: {
1537
+ type: "object",
1538
+ properties: {
1539
+ post_id: {
1540
+ type: "string",
1541
+ description: "The ID of the post"
1542
+ },
1543
+ subreddit: {
1544
+ type: "string",
1545
+ description: "The subreddit where the post is located"
1546
+ },
1547
+ sort: {
1548
+ type: "string",
1549
+ description: "Comment sort order: best, top, new, controversial, old, qa",
1550
+ enum: ["best", "top", "new", "controversial", "old", "qa"],
1551
+ default: "best"
1552
+ },
1553
+ limit: {
1554
+ type: "number",
1555
+ description: "Maximum number of comments to load",
1556
+ minimum: 1,
1557
+ maximum: 500,
1558
+ default: 100
1559
+ }
1560
+ },
1561
+ required: ["post_id", "subreddit"]
1562
+ }
1563
+ },
1564
+ {
1565
+ name: "get_user_posts",
1566
+ description: "Get posts submitted by a specific user",
1567
+ inputSchema: {
1568
+ type: "object",
1569
+ properties: {
1570
+ username: {
1571
+ type: "string",
1572
+ description: "The username to get posts for"
1573
+ },
1574
+ sort: {
1575
+ type: "string",
1576
+ description: "Sort order: new, hot, top, controversial",
1577
+ enum: ["new", "hot", "top", "controversial"],
1578
+ default: "new"
1579
+ },
1580
+ time_filter: {
1581
+ type: "string",
1582
+ description: "Time filter for top/controversial: hour, day, week, month, year, all",
1583
+ enum: ["hour", "day", "week", "month", "year", "all"],
1584
+ default: "all"
1585
+ },
1586
+ limit: {
1587
+ type: "number",
1588
+ description: "Maximum number of posts to return",
1589
+ minimum: 1,
1590
+ maximum: 100,
1591
+ default: 10
1592
+ }
1593
+ },
1594
+ required: ["username"]
1595
+ }
1596
+ },
1597
+ {
1598
+ name: "get_user_comments",
1599
+ description: "Get comments made by a specific user",
1600
+ inputSchema: {
1601
+ type: "object",
1602
+ properties: {
1603
+ username: {
1604
+ type: "string",
1605
+ description: "The username to get comments for"
1606
+ },
1607
+ sort: {
1608
+ type: "string",
1609
+ description: "Sort order: new, hot, top, controversial",
1610
+ enum: ["new", "hot", "top", "controversial"],
1611
+ default: "new"
1612
+ },
1613
+ time_filter: {
1614
+ type: "string",
1615
+ description: "Time filter for top/controversial: hour, day, week, month, year, all",
1616
+ enum: ["hour", "day", "week", "month", "year", "all"],
1617
+ default: "all"
1618
+ },
1619
+ limit: {
1620
+ type: "number",
1621
+ description: "Maximum number of comments to return",
1622
+ minimum: 1,
1623
+ maximum: 100,
1624
+ default: 10
1625
+ }
1626
+ },
1627
+ required: ["username"]
1628
+ }
1629
+ }
1630
+ ]
1631
+ }));
1632
+ server.setRequestHandler(import_types6.CallToolRequestSchema, async (request) => {
1633
+ try {
1634
+ const toolName = request.params.name;
1635
+ const toolParams = request.params.arguments || {};
1636
+ switch (toolName) {
1637
+ case "test_reddit_mcp_server":
1638
+ return {
1639
+ content: [
1640
+ {
1641
+ type: "text",
1642
+ text: "Hello, world! The Reddit MCP Server is working correctly."
1643
+ }
1644
+ ]
1645
+ };
1646
+ case "get_reddit_post":
1647
+ return await getRedditPost(toolParams);
1648
+ case "get_top_posts":
1649
+ return await getTopPosts(
1650
+ toolParams
1651
+ );
1652
+ case "get_user_info":
1653
+ return await getUserInfo(toolParams);
1654
+ case "get_subreddit_info":
1655
+ return await getSubredditInfo(toolParams);
1656
+ case "get_trending_subreddits":
1657
+ return await getTrendingSubreddits();
1658
+ case "create_post":
1659
+ return await createPost(
1660
+ toolParams
1661
+ );
1662
+ case "reply_to_post":
1663
+ return await replyToPost(
1664
+ toolParams
1665
+ );
1666
+ case "search_reddit":
1667
+ return await searchReddit(
1668
+ toolParams
1669
+ );
1670
+ case "get_post_comments":
1671
+ return await getPostComments(
1672
+ toolParams
1673
+ );
1674
+ case "get_user_posts":
1675
+ return await getUserPosts(
1676
+ toolParams
1677
+ );
1678
+ case "get_user_comments":
1679
+ return await getUserComments(
1680
+ toolParams
1681
+ );
1682
+ default:
1683
+ throw new import_types6.McpError(import_types6.ErrorCode.MethodNotFound, `Tool with name ${toolName} not found`);
1684
+ }
1685
+ } catch (error) {
1686
+ if (error instanceof Error) {
1687
+ throw new import_types6.McpError(import_types6.ErrorCode.InternalError, `Failed to fetch data: ${error.message}`);
1688
+ }
1689
+ throw error;
1690
+ }
1691
+ });
1692
+ return server;
1693
+ }
1694
+ function initializeReddit() {
1695
+ const clientId = process.env.REDDIT_CLIENT_ID;
1696
+ const clientSecret = process.env.REDDIT_CLIENT_SECRET;
1697
+ const userAgent = process.env.REDDIT_USER_AGENT || "RedditMCPServer/0.1.0";
1698
+ const username = process.env.REDDIT_USERNAME;
1699
+ const password = process.env.REDDIT_PASSWORD;
1700
+ if (!clientId || !clientSecret) {
1701
+ console.error(
1702
+ "[Error] Missing required Reddit API credentials. Please set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables."
1703
+ );
1704
+ process.exit(1);
1705
+ }
1706
+ try {
1707
+ initializeRedditClient({
1708
+ clientId,
1709
+ clientSecret,
1710
+ userAgent,
1711
+ username,
1712
+ password
1713
+ });
1714
+ console.log("[Setup] Reddit client initialized");
1715
+ if (username && password) {
1716
+ console.log(`[Setup] Authenticated as user: ${username}`);
1717
+ } else {
1718
+ console.log("[Setup] Running in read-only mode (no user authentication)");
1719
+ }
1720
+ } catch (error) {
1721
+ console.error("[Error] Failed to initialize Reddit client:", error);
1722
+ process.exit(1);
1723
+ }
1724
+ }
1725
+ app.all("/mcp", async (c) => {
1726
+ console.log(`[MCP] Request received: ${c.req.method} ${c.req.url}`);
1727
+ try {
1728
+ const mcpServer = createMCPServer();
1729
+ const transport = new import_mcp.StreamableHTTPTransport();
1730
+ await mcpServer.connect(transport);
1731
+ return transport.handleRequest(c);
1732
+ } catch (error) {
1733
+ console.error("[MCP] Request handling failed:", error);
1734
+ return c.json({ error: "Internal server error" }, 500);
1735
+ }
1736
+ });
1737
+ initializeReddit();
1738
+ function startServer(port = 3e3) {
1739
+ const server = (0, import_node_server.serve)({
1740
+ fetch: app.fetch,
1741
+ port
1742
+ });
1743
+ console.log(`[Server] Reddit MCP Server running at http://localhost:${port}`);
1744
+ console.log(`[Server] MCP endpoint available at http://localhost:${port}/mcp`);
1745
+ return server;
1746
+ }
1747
+ if (require.main === module) {
1748
+ const port = parseInt(process.env.PORT || "3000", 10);
1749
+ startServer(port);
1750
+ }
1751
+ // Annotate the CommonJS export names for ESM import in node:
1752
+ 0 && (module.exports = {
1753
+ startServer
1754
+ });