reddit-mcp-server 1.1.1 → 1.2.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/index.js CHANGED
@@ -1,856 +1,984 @@
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 __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (let key of __getOwnPropNames(from))
11
- if (!__hasOwnProp.call(to, key) && key !== except)
12
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
- }
14
- return to;
15
- };
16
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
21
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
- mod
23
- ));
24
-
25
- // src/index.ts
26
- var import_fastmcp = require("fastmcp");
27
- var import_zod = require("zod");
1
+ const require_chunk = require('./chunk-kSYXY2_d.js');
2
+ let fastmcp = require("fastmcp");
3
+ let zod = require("zod");
4
+ let crypto = require("crypto");
5
+ crypto = require_chunk.__toESM(crypto);
6
+ let dotenv = require("dotenv");
7
+ dotenv = require_chunk.__toESM(dotenv);
28
8
 
29
- // src/client/reddit-client.ts
9
+ //#region src/client/reddit-client.ts
30
10
  var RedditClient = class {
31
- clientId;
32
- clientSecret;
33
- userAgent;
34
- username;
35
- password;
36
- accessToken;
37
- tokenExpiry = 0;
38
- baseUrl = "https://oauth.reddit.com";
39
- authenticated = false;
40
- constructor(config) {
41
- this.clientId = config.clientId;
42
- this.clientSecret = config.clientSecret;
43
- this.userAgent = config.userAgent;
44
- this.username = config.username;
45
- this.password = config.password;
46
- }
47
- async makeRequest(path, options = {}) {
48
- if (Date.now() >= this.tokenExpiry || !this.authenticated) {
49
- await this.authenticate();
50
- }
51
- const url = `${this.baseUrl}${path}`;
52
- const headers = {
53
- "User-Agent": this.userAgent,
54
- Authorization: `Bearer ${this.accessToken}`,
55
- ...options.headers
56
- };
57
- const response = await fetch(url, {
58
- ...options,
59
- headers
60
- });
61
- if (response.status === 401 && this.authenticated) {
62
- await this.authenticate();
63
- const retryHeaders = {
64
- ...headers,
65
- Authorization: `Bearer ${this.accessToken}`
66
- };
67
- return fetch(url, {
68
- ...options,
69
- headers: retryHeaders
70
- });
71
- }
72
- return response;
73
- }
74
- async authenticate() {
75
- try {
76
- const now = Date.now();
77
- if (this.accessToken && now < this.tokenExpiry) {
78
- return;
79
- }
80
- const authUrl = "https://www.reddit.com/api/v1/access_token";
81
- const authData = new URLSearchParams();
82
- if (this.username && this.password) {
83
- authData.append("grant_type", "password");
84
- authData.append("username", this.username);
85
- authData.append("password", this.password);
86
- } else {
87
- authData.append("grant_type", "client_credentials");
88
- }
89
- const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
90
- const response = await fetch(authUrl, {
91
- method: "POST",
92
- headers: {
93
- "User-Agent": this.userAgent,
94
- "Content-Type": "application/x-www-form-urlencoded",
95
- Authorization: `Basic ${credentials}`
96
- },
97
- body: authData.toString()
98
- });
99
- if (!response.ok) {
100
- throw new Error(`Authentication failed: ${response.status}`);
101
- }
102
- const data = await response.json();
103
- this.accessToken = data.access_token;
104
- this.tokenExpiry = now + data.expires_in * 1e3;
105
- this.authenticated = true;
106
- } catch {
107
- throw new Error("Failed to authenticate with Reddit API");
108
- }
109
- }
110
- async checkAuthentication() {
111
- if (!this.authenticated) {
112
- try {
113
- await this.authenticate();
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- }
119
- return true;
120
- }
121
- async getUser(username) {
122
- await this.authenticate();
123
- try {
124
- const response = await this.makeRequest(`/user/${username}/about.json`);
125
- if (!response.ok) {
126
- throw new Error(`HTTP ${response.status}`);
127
- }
128
- const json = await response.json();
129
- const data = json.data;
130
- return {
131
- name: data.name,
132
- id: data.id,
133
- commentKarma: data.comment_karma,
134
- linkKarma: data.link_karma,
135
- totalKarma: data.total_karma || data.comment_karma + data.link_karma,
136
- isMod: data.is_mod,
137
- isGold: data.is_gold,
138
- isEmployee: data.is_employee,
139
- createdUtc: data.created_utc,
140
- profileUrl: `https://reddit.com/user/${data.name}`
141
- };
142
- } catch {
143
- throw new Error(`Failed to get user info for ${username}`);
144
- }
145
- }
146
- async getSubredditInfo(subredditName) {
147
- await this.authenticate();
148
- try {
149
- const response = await this.makeRequest(`/r/${subredditName}/about.json`);
150
- if (!response.ok) {
151
- throw new Error(`HTTP ${response.status}`);
152
- }
153
- const json = await response.json();
154
- const data = json.data;
155
- return {
156
- displayName: data.display_name,
157
- title: data.title,
158
- description: data.description || "",
159
- publicDescription: data.public_description || "",
160
- subscribers: data.subscribers,
161
- activeUserCount: data.active_user_count ?? void 0,
162
- createdUtc: data.created_utc,
163
- over18: data.over18,
164
- subredditType: data.subreddit_type,
165
- url: data.url
166
- };
167
- } catch {
168
- throw new Error(`Failed to get subreddit info for ${subredditName}`);
169
- }
170
- }
171
- async getTopPosts(subreddit, timeFilter = "week", limit = 10) {
172
- await this.authenticate();
173
- try {
174
- const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
175
- const params = new URLSearchParams({
176
- t: timeFilter,
177
- limit: limit.toString()
178
- });
179
- const response = await this.makeRequest(`${endpoint}?${params}`);
180
- if (!response.ok) {
181
- throw new Error(`HTTP ${response.status}`);
182
- }
183
- const json = await response.json();
184
- return json.data.children.map((child) => {
185
- const post = child.data;
186
- return {
187
- id: post.id,
188
- title: post.title,
189
- author: post.author,
190
- subreddit: post.subreddit,
191
- selftext: post.selftext,
192
- url: post.url,
193
- score: post.score,
194
- upvoteRatio: post.upvote_ratio,
195
- numComments: post.num_comments,
196
- createdUtc: post.created_utc,
197
- over18: post.over_18,
198
- spoiler: post.spoiler,
199
- edited: !!post.edited,
200
- isSelf: post.is_self,
201
- linkFlairText: post.link_flair_text ?? void 0,
202
- permalink: post.permalink
203
- };
204
- });
205
- } catch {
206
- throw new Error(`Failed to get top posts for ${subreddit || "home"}`);
207
- }
208
- }
209
- async getPost(postId, subreddit) {
210
- await this.authenticate();
211
- try {
212
- const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
213
- const response = await this.makeRequest(endpoint);
214
- if (!response.ok) {
215
- throw new Error(`HTTP ${response.status}`);
216
- }
217
- const json = await response.json();
218
- let post;
219
- if (subreddit) {
220
- post = json[0].data.children[0].data;
221
- } else {
222
- if (!json.data.children.length) {
223
- throw new Error(`Post with ID ${postId} not found`);
224
- }
225
- post = json.data.children[0].data;
226
- }
227
- return {
228
- id: post.id,
229
- title: post.title,
230
- author: post.author,
231
- subreddit: post.subreddit,
232
- selftext: post.selftext,
233
- url: post.url,
234
- score: post.score,
235
- upvoteRatio: post.upvote_ratio,
236
- numComments: post.num_comments,
237
- createdUtc: post.created_utc,
238
- over18: post.over_18,
239
- spoiler: post.spoiler,
240
- edited: !!post.edited,
241
- isSelf: post.is_self,
242
- linkFlairText: post.link_flair_text,
243
- permalink: post.permalink
244
- };
245
- } catch {
246
- throw new Error(`Failed to get post with ID ${postId}`);
247
- }
248
- }
249
- async getTrendingSubreddits(limit = 5) {
250
- await this.authenticate();
251
- try {
252
- const params = new URLSearchParams({ limit: limit.toString() });
253
- const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
254
- if (!response.ok) {
255
- throw new Error(`HTTP ${response.status}`);
256
- }
257
- const json = await response.json();
258
- return json.data.children.map((child) => child.data.display_name);
259
- } catch {
260
- throw new Error("Failed to get trending subreddits");
261
- }
262
- }
263
- async createPost(subreddit, title, content, isSelf = true) {
264
- await this.authenticate();
265
- if (!this.username || !this.password) {
266
- throw new Error("User authentication required for posting");
267
- }
268
- try {
269
- const kind = isSelf ? "self" : "link";
270
- const params = new URLSearchParams();
271
- params.append("sr", subreddit);
272
- params.append("kind", kind);
273
- params.append("title", title);
274
- params.append(isSelf ? "text" : "url", content);
275
- const response = await this.makeRequest("/api/submit", {
276
- method: "POST",
277
- headers: {
278
- "Content-Type": "application/x-www-form-urlencoded"
279
- },
280
- body: params.toString()
281
- });
282
- if (!response.ok) {
283
- throw new Error(`HTTP ${response.status}`);
284
- }
285
- const json = await response.json();
286
- if (json.success) {
287
- const postId = json.data.id;
288
- return await this.getPost(postId);
289
- } else {
290
- throw new Error("Failed to create post");
291
- }
292
- } catch {
293
- throw new Error(`Failed to create post in ${subreddit}`);
294
- }
295
- }
296
- async checkPostExists(postId) {
297
- await this.authenticate();
298
- try {
299
- const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
300
- if (!response.ok) {
301
- return false;
302
- }
303
- const json = await response.json();
304
- return json.data.children.length > 0;
305
- } catch {
306
- return false;
307
- }
308
- }
309
- async replyToPost(postId, content) {
310
- await this.authenticate();
311
- if (!this.username || !this.password) {
312
- throw new Error("User authentication required for posting replies");
313
- }
314
- try {
315
- if (!await this.checkPostExists(postId)) {
316
- throw new Error(`Post with ID ${postId} does not exist or is not accessible`);
317
- }
318
- const params = new URLSearchParams();
319
- params.append("thing_id", `t3_${postId}`);
320
- params.append("text", content);
321
- const response = await this.makeRequest("/api/comment", {
322
- method: "POST",
323
- headers: {
324
- "Content-Type": "application/x-www-form-urlencoded"
325
- },
326
- body: params.toString()
327
- });
328
- if (!response.ok) {
329
- throw new Error(`HTTP ${response.status}`);
330
- }
331
- const commentData = await response.json();
332
- return {
333
- id: commentData.id,
334
- author: this.username,
335
- body: content,
336
- score: 1,
337
- controversiality: 0,
338
- subreddit: commentData.subreddit,
339
- submissionTitle: commentData.link_title,
340
- createdUtc: Date.now() / 1e3,
341
- edited: false,
342
- isSubmitter: false,
343
- permalink: commentData.permalink
344
- };
345
- } catch {
346
- throw new Error(`Failed to reply to post ${postId}`);
347
- }
348
- }
349
- async searchReddit(query, options = {}) {
350
- await this.authenticate();
351
- try {
352
- const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
353
- const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
354
- const params = new URLSearchParams({
355
- q: query,
356
- sort,
357
- t: timeFilter,
358
- limit: limit.toString(),
359
- type,
360
- ...subreddit && { restrict_sr: "true" }
361
- });
362
- const response = await this.makeRequest(`${endpoint}?${params}`);
363
- if (!response.ok) {
364
- throw new Error(`HTTP ${response.status}`);
365
- }
366
- const json = await response.json();
367
- return json.data.children.filter((child) => child.kind === "t3").map((child) => {
368
- const post = child.data;
369
- return {
370
- id: post.id,
371
- title: post.title,
372
- author: post.author,
373
- subreddit: post.subreddit,
374
- selftext: post.selftext || "",
375
- url: post.url,
376
- score: post.score,
377
- upvoteRatio: post.upvote_ratio,
378
- numComments: post.num_comments,
379
- createdUtc: post.created_utc,
380
- over18: post.over_18,
381
- spoiler: post.spoiler,
382
- edited: !!post.edited,
383
- isSelf: post.is_self,
384
- linkFlairText: post.link_flair_text ?? void 0,
385
- permalink: post.permalink
386
- };
387
- });
388
- } catch {
389
- throw new Error(`Failed to search Reddit for: ${query}`);
390
- }
391
- }
392
- async getPostComments(postId, subreddit, options = {}) {
393
- await this.authenticate();
394
- try {
395
- const { sort = "best", limit = 100 } = options;
396
- const params = new URLSearchParams({
397
- sort,
398
- limit: limit.toString()
399
- });
400
- const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
401
- if (!response.ok) {
402
- throw new Error(`HTTP ${response.status}`);
403
- }
404
- const json = await response.json();
405
- const postData = json[0].data.children[0].data;
406
- const post = {
407
- id: postData.id,
408
- title: postData.title,
409
- author: postData.author,
410
- subreddit: postData.subreddit,
411
- selftext: postData.selftext || "",
412
- url: postData.url,
413
- score: postData.score,
414
- upvoteRatio: postData.upvote_ratio,
415
- numComments: postData.num_comments,
416
- createdUtc: postData.created_utc,
417
- over18: postData.over_18,
418
- spoiler: postData.spoiler,
419
- edited: !!postData.edited,
420
- isSelf: postData.is_self,
421
- linkFlairText: postData.link_flair_text,
422
- permalink: postData.permalink
423
- };
424
- const comments = [];
425
- const parseComments = (commentData, depth = 0) => {
426
- for (const item of commentData) {
427
- if (item.kind === "t1" && item.data.body) {
428
- comments.push({
429
- id: item.data.id,
430
- author: item.data.author,
431
- body: item.data.body,
432
- score: item.data.score,
433
- controversiality: item.data.controversiality,
434
- subreddit: item.data.subreddit,
435
- submissionTitle: post.title,
436
- createdUtc: item.data.created_utc,
437
- edited: !!item.data.edited,
438
- isSubmitter: item.data.is_submitter,
439
- permalink: item.data.permalink,
440
- depth,
441
- parentId: item.data.parent_id
442
- });
443
- if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
444
- parseComments(item.data.replies.data.children, depth + 1);
445
- }
446
- }
447
- }
448
- };
449
- if (json[1] && json[1].data && json[1].data.children) {
450
- parseComments(json[1].data.children);
451
- }
452
- return { post, comments };
453
- } catch {
454
- throw new Error(`Failed to get comments for post ${postId}`);
455
- }
456
- }
457
- async getUserPosts(username, options = {}) {
458
- await this.authenticate();
459
- try {
460
- const { sort = "new", timeFilter = "all", limit = 25 } = options;
461
- const params = new URLSearchParams({
462
- sort,
463
- t: timeFilter,
464
- limit: limit.toString()
465
- });
466
- const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
467
- if (!response.ok) {
468
- throw new Error(`HTTP ${response.status}`);
469
- }
470
- const json = await response.json();
471
- return json.data.children.filter((child) => child.kind === "t3").map((child) => {
472
- const post = child.data;
473
- return {
474
- id: post.id,
475
- title: post.title,
476
- author: post.author,
477
- subreddit: post.subreddit,
478
- selftext: post.selftext || "",
479
- url: post.url,
480
- score: post.score,
481
- upvoteRatio: post.upvote_ratio,
482
- numComments: post.num_comments,
483
- createdUtc: post.created_utc,
484
- over18: post.over_18,
485
- spoiler: post.spoiler,
486
- edited: !!post.edited,
487
- isSelf: post.is_self,
488
- linkFlairText: post.link_flair_text ?? void 0,
489
- permalink: post.permalink
490
- };
491
- });
492
- } catch {
493
- throw new Error(`Failed to get posts for user ${username}`);
494
- }
495
- }
496
- async getUserComments(username, options = {}) {
497
- await this.authenticate();
498
- try {
499
- const { sort = "new", timeFilter = "all", limit = 25 } = options;
500
- const params = new URLSearchParams({
501
- sort,
502
- t: timeFilter,
503
- limit: limit.toString()
504
- });
505
- const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
506
- if (!response.ok) {
507
- throw new Error(`HTTP ${response.status}`);
508
- }
509
- const json = await response.json();
510
- return json.data.children.filter((child) => child.kind === "t1").map((child) => {
511
- const comment = child.data;
512
- return {
513
- id: comment.id,
514
- author: comment.author,
515
- body: comment.body,
516
- score: comment.score,
517
- controversiality: comment.controversiality,
518
- subreddit: comment.subreddit,
519
- submissionTitle: comment.link_title || "",
520
- createdUtc: comment.created_utc,
521
- edited: !!comment.edited,
522
- isSubmitter: comment.is_submitter,
523
- permalink: comment.permalink
524
- };
525
- });
526
- } catch {
527
- throw new Error(`Failed to get comments for user ${username}`);
528
- }
529
- }
11
+ clientId;
12
+ clientSecret;
13
+ userAgent;
14
+ username;
15
+ password;
16
+ accessToken;
17
+ tokenExpiry = 0;
18
+ baseUrl;
19
+ authenticated = false;
20
+ authMode;
21
+ hasCredentials;
22
+ safeMode;
23
+ lastWriteTime = 0;
24
+ recentContentHashes = /* @__PURE__ */ new Set();
25
+ constructor(config) {
26
+ this.clientId = config.clientId;
27
+ this.clientSecret = config.clientSecret;
28
+ this.userAgent = config.userAgent;
29
+ this.username = config.username;
30
+ this.password = config.password;
31
+ this.authMode = config.authMode || "auto";
32
+ this.hasCredentials = !!(this.clientId && this.clientSecret);
33
+ this.baseUrl = this.determineBaseUrl();
34
+ this.safeMode = config.safeMode || {
35
+ enabled: false,
36
+ mode: "off",
37
+ writeDelayMs: 0,
38
+ duplicateCheck: false,
39
+ maxRecentHashes: 10
40
+ };
41
+ }
42
+ determineBaseUrl() {
43
+ switch (this.authMode) {
44
+ case "authenticated": return "https://oauth.reddit.com";
45
+ case "anonymous": return "https://www.reddit.com";
46
+ case "auto": return this.hasCredentials ? "https://oauth.reddit.com" : "https://www.reddit.com";
47
+ }
48
+ }
49
+ async makeRequest(path, options = {}) {
50
+ const requiresAuth = this.authMode === "authenticated" || this.authMode === "auto" && this.hasCredentials;
51
+ if (requiresAuth && (Date.now() >= this.tokenExpiry || !this.authenticated)) await this.authenticate();
52
+ const url = `${this.baseUrl}${path}`;
53
+ const headers = {
54
+ "User-Agent": this.userAgent,
55
+ ...options.headers
56
+ };
57
+ if (requiresAuth && this.accessToken) headers.Authorization = `Bearer ${this.accessToken}`;
58
+ const response = await fetch(url, {
59
+ ...options,
60
+ headers
61
+ });
62
+ if (response.status === 401 && this.authenticated) {
63
+ await this.authenticate();
64
+ const retryHeaders = {
65
+ ...headers,
66
+ Authorization: `Bearer ${this.accessToken}`
67
+ };
68
+ return fetch(url, {
69
+ ...options,
70
+ headers: retryHeaders
71
+ });
72
+ }
73
+ return response;
74
+ }
75
+ async authenticate() {
76
+ if (this.authMode === "anonymous") {
77
+ this.authenticated = false;
78
+ return;
79
+ }
80
+ if (this.authMode === "authenticated" && !this.hasCredentials) throw new Error("Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
81
+ if (this.authMode === "auto" && !this.hasCredentials) {
82
+ this.authenticated = false;
83
+ return;
84
+ }
85
+ try {
86
+ const now = Date.now();
87
+ if (this.accessToken && now < this.tokenExpiry) return;
88
+ const authUrl = "https://www.reddit.com/api/v1/access_token";
89
+ const authData = new URLSearchParams();
90
+ const username = this.username;
91
+ const password = this.password;
92
+ if (!!(username && password)) {
93
+ authData.append("grant_type", "password");
94
+ authData.append("username", username);
95
+ authData.append("password", password);
96
+ } else authData.append("grant_type", "client_credentials");
97
+ const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
98
+ const response = await fetch(authUrl, {
99
+ method: "POST",
100
+ headers: {
101
+ "User-Agent": this.userAgent,
102
+ "Content-Type": "application/x-www-form-urlencoded",
103
+ Authorization: `Basic ${credentials}`
104
+ },
105
+ body: authData.toString()
106
+ });
107
+ if (!response.ok) {
108
+ const statusText = response.statusText || "Unknown Error";
109
+ throw new Error(`Authentication failed: ${response.status} ${statusText}`);
110
+ }
111
+ const data = await response.json();
112
+ this.accessToken = data.access_token;
113
+ this.tokenExpiry = now + data.expires_in * 1e3;
114
+ this.authenticated = true;
115
+ } catch (error) {
116
+ if (error instanceof Error) throw error;
117
+ throw new Error("Failed to authenticate with Reddit API");
118
+ }
119
+ }
120
+ async checkAuthentication() {
121
+ if (!this.authenticated) try {
122
+ await this.authenticate();
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+ validateWriteAccess() {
130
+ if (!this.username || !this.password) {
131
+ if (this.authMode === "anonymous") throw new Error("Write operations not available in anonymous mode. Set REDDIT_USERNAME, REDDIT_PASSWORD and use 'auto' or 'authenticated' mode.");
132
+ throw new Error("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
133
+ }
134
+ }
135
+ async enforceWriteRateLimit() {
136
+ if (!this.safeMode.enabled || this.safeMode.writeDelayMs <= 0) return;
137
+ const elapsed = Date.now() - this.lastWriteTime;
138
+ if (elapsed < this.safeMode.writeDelayMs) {
139
+ const waitTime = this.safeMode.writeDelayMs - elapsed;
140
+ console.error(`[SafeMode] Rate limit: waiting ${waitTime}ms before write operation`);
141
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
142
+ }
143
+ this.lastWriteTime = Date.now();
144
+ }
145
+ hashContent(content) {
146
+ return crypto.default.createHash("md5").update(content.trim().toLowerCase()).digest("hex");
147
+ }
148
+ checkDuplicateContent(content) {
149
+ if (!this.safeMode.enabled || !this.safeMode.duplicateCheck) return;
150
+ const hash = this.hashContent(content);
151
+ if (this.recentContentHashes.has(hash)) throw new Error("Duplicate content detected. Reddit's spam filter may ban your account for posting identical content. Please modify your content and try again.");
152
+ this.recentContentHashes.add(hash);
153
+ if (this.recentContentHashes.size > this.safeMode.maxRecentHashes) {
154
+ const first = this.recentContentHashes.values().next().value;
155
+ if (first) this.recentContentHashes.delete(first);
156
+ }
157
+ }
158
+ async getUser(username) {
159
+ try {
160
+ const response = await this.makeRequest(`/user/${username}/about.json`);
161
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
162
+ const data = (await response.json()).data;
163
+ return {
164
+ name: data.name,
165
+ id: data.id,
166
+ commentKarma: data.comment_karma,
167
+ linkKarma: data.link_karma,
168
+ totalKarma: data.total_karma || data.comment_karma + data.link_karma,
169
+ isMod: data.is_mod,
170
+ isGold: data.is_gold,
171
+ isEmployee: data.is_employee,
172
+ createdUtc: data.created_utc,
173
+ profileUrl: `https://reddit.com/user/${data.name}`
174
+ };
175
+ } catch {
176
+ throw new Error(`Failed to get user info for ${username}`);
177
+ }
178
+ }
179
+ async getSubredditInfo(subredditName) {
180
+ try {
181
+ const response = await this.makeRequest(`/r/${subredditName}/about.json`);
182
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
183
+ const data = (await response.json()).data;
184
+ return {
185
+ displayName: data.display_name,
186
+ title: data.title,
187
+ description: data.description || "",
188
+ publicDescription: data.public_description || "",
189
+ subscribers: data.subscribers,
190
+ activeUserCount: data.active_user_count ?? void 0,
191
+ createdUtc: data.created_utc,
192
+ over18: data.over18,
193
+ subredditType: data.subreddit_type,
194
+ url: data.url
195
+ };
196
+ } catch {
197
+ throw new Error(`Failed to get subreddit info for ${subredditName}`);
198
+ }
199
+ }
200
+ async getTopPosts(subreddit, timeFilter = "week", limit = 10) {
201
+ try {
202
+ const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
203
+ const params = new URLSearchParams({
204
+ t: timeFilter,
205
+ limit: limit.toString()
206
+ });
207
+ const response = await this.makeRequest(`${endpoint}?${params}`);
208
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
209
+ return (await response.json()).data.children.map((child) => {
210
+ const post = child.data;
211
+ return {
212
+ id: post.id,
213
+ title: post.title,
214
+ author: post.author,
215
+ subreddit: post.subreddit,
216
+ selftext: post.selftext,
217
+ url: post.url,
218
+ score: post.score,
219
+ upvoteRatio: post.upvote_ratio,
220
+ numComments: post.num_comments,
221
+ createdUtc: post.created_utc,
222
+ over18: post.over_18,
223
+ spoiler: post.spoiler,
224
+ edited: !!post.edited,
225
+ isSelf: post.is_self,
226
+ linkFlairText: post.link_flair_text ?? void 0,
227
+ permalink: post.permalink
228
+ };
229
+ });
230
+ } catch {
231
+ throw new Error(`Failed to get top posts for ${subreddit || "home"}`);
232
+ }
233
+ }
234
+ async getPost(postId, subreddit) {
235
+ try {
236
+ const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
237
+ const response = await this.makeRequest(endpoint);
238
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
239
+ let post;
240
+ if (subreddit) post = (await response.json())[0].data.children[0].data;
241
+ else {
242
+ const json = await response.json();
243
+ if (!json.data.children.length) throw new Error(`Post with ID ${postId} not found`);
244
+ post = json.data.children[0].data;
245
+ }
246
+ return {
247
+ id: post.id,
248
+ title: post.title,
249
+ author: post.author,
250
+ subreddit: post.subreddit,
251
+ selftext: post.selftext,
252
+ url: post.url,
253
+ score: post.score,
254
+ upvoteRatio: post.upvote_ratio,
255
+ numComments: post.num_comments,
256
+ createdUtc: post.created_utc,
257
+ over18: post.over_18,
258
+ spoiler: post.spoiler,
259
+ edited: !!post.edited,
260
+ isSelf: post.is_self,
261
+ linkFlairText: post.link_flair_text ?? void 0,
262
+ permalink: post.permalink
263
+ };
264
+ } catch {
265
+ throw new Error(`Failed to get post with ID ${postId}`);
266
+ }
267
+ }
268
+ async getTrendingSubreddits(limit = 5) {
269
+ try {
270
+ const params = new URLSearchParams({ limit: limit.toString() });
271
+ const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
272
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
273
+ return (await response.json()).data.children.map((child) => child.data.display_name);
274
+ } catch {
275
+ throw new Error("Failed to get trending subreddits");
276
+ }
277
+ }
278
+ async createPost(subreddit, title, content, isSelf = true) {
279
+ this.validateWriteAccess();
280
+ await this.enforceWriteRateLimit();
281
+ this.checkDuplicateContent(title + content);
282
+ try {
283
+ var _json$json, _json$json2, _json$json3;
284
+ const kind = isSelf ? "self" : "link";
285
+ const params = new URLSearchParams();
286
+ params.append("sr", subreddit);
287
+ params.append("kind", kind);
288
+ params.append("title", title);
289
+ params.append(isSelf ? "text" : "url", content);
290
+ params.append("api_type", "json");
291
+ const response = await this.makeRequest("/api/submit", {
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
294
+ body: params.toString()
295
+ });
296
+ if (!response.ok) {
297
+ const errorText = await response.text();
298
+ console.error(`[Reddit API] Create post failed: ${response.status} ${response.statusText}`);
299
+ console.error(`[Reddit API] Error response: ${errorText}`);
300
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
301
+ }
302
+ const json = await response.json();
303
+ console.error(`[Reddit API] Create post response:`, JSON.stringify(json, null, 2));
304
+ if (((_json$json = json.json) === null || _json$json === void 0 ? void 0 : _json$json.errors) && json.json.errors.length > 0) {
305
+ const errors = json.json.errors.map((e) => e.join(": ")).join(", ");
306
+ console.error(`[Reddit API] Post creation errors: ${errors}`);
307
+ throw new Error(`Reddit API errors: ${errors}`);
308
+ }
309
+ const postId = ((_json$json2 = json.json) === null || _json$json2 === void 0 || (_json$json2 = _json$json2.data) === null || _json$json2 === void 0 ? void 0 : _json$json2.id) || ((_json$json3 = json.json) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.data) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.name) === null || _json$json3 === void 0 ? void 0 : _json$json3.replace("t3_", ""));
310
+ if (!postId) {
311
+ console.error(`[Reddit API] No post ID in response`);
312
+ throw new Error("No post ID returned from Reddit");
313
+ }
314
+ console.error(`[Reddit API] Post created with ID: ${postId}`);
315
+ return await this.getPost(postId, subreddit);
316
+ } catch (error) {
317
+ console.error(`[Reddit API] Create post exception:`, error);
318
+ if (error instanceof Error && error.message.includes("HTTP")) throw error;
319
+ throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`);
320
+ }
321
+ }
322
+ async checkPostExists(postId) {
323
+ try {
324
+ const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
325
+ if (!response.ok) return false;
326
+ return (await response.json()).data.children.length > 0;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+ async replyToPost(postId, content) {
332
+ this.validateWriteAccess();
333
+ await this.enforceWriteRateLimit();
334
+ this.checkDuplicateContent(content);
335
+ try {
336
+ var _json$json4, _json$json5;
337
+ if (!await this.checkPostExists(postId)) throw new Error(`Post with ID ${postId} does not exist or is not accessible`);
338
+ const params = new URLSearchParams();
339
+ params.append("thing_id", `t3_${postId}`);
340
+ params.append("text", content);
341
+ params.append("api_type", "json");
342
+ const response = await this.makeRequest("/api/comment", {
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
345
+ body: params.toString()
346
+ });
347
+ if (!response.ok) {
348
+ const errorText = await response.text();
349
+ console.error(`[Reddit API] Reply to post failed: ${response.status} ${response.statusText}`);
350
+ console.error(`[Reddit API] Error response: ${errorText}`);
351
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
352
+ }
353
+ const json = await response.json();
354
+ console.error(`[Reddit API] Reply response:`, JSON.stringify(json, null, 2));
355
+ if (((_json$json4 = json.json) === null || _json$json4 === void 0 || (_json$json4 = _json$json4.data) === null || _json$json4 === void 0 ? void 0 : _json$json4.things) && json.json.data.things.length > 0) {
356
+ const commentData = json.json.data.things[0].data;
357
+ const author = this.username ?? "[unknown]";
358
+ return {
359
+ id: commentData.id,
360
+ author,
361
+ body: content,
362
+ score: 1,
363
+ controversiality: 0,
364
+ subreddit: commentData.subreddit,
365
+ submissionTitle: commentData.link_title ?? "",
366
+ createdUtc: Date.now() / 1e3,
367
+ edited: false,
368
+ isSubmitter: false,
369
+ permalink: commentData.permalink
370
+ };
371
+ } else if (((_json$json5 = json.json) === null || _json$json5 === void 0 ? void 0 : _json$json5.errors) && json.json.errors.length > 0) {
372
+ const errors = json.json.errors.map((e) => e.join(": ")).join(", ");
373
+ console.error(`[Reddit API] Reply errors: ${errors}`);
374
+ throw new Error(`Reddit API errors: ${errors}`);
375
+ } else {
376
+ console.error(`[Reddit API] Unexpected reply response format`);
377
+ throw new Error("Failed to parse reply response");
378
+ }
379
+ } catch (error) {
380
+ console.error(`[Reddit API] Reply to post exception:`, error);
381
+ if (error instanceof Error && error.message.includes("HTTP")) throw error;
382
+ throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`);
383
+ }
384
+ }
385
+ async deletePost(thingId) {
386
+ this.validateWriteAccess();
387
+ try {
388
+ const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
389
+ const params = new URLSearchParams();
390
+ params.append("id", fullThingId);
391
+ const response = await this.makeRequest("/api/del", {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
394
+ body: params.toString()
395
+ });
396
+ if (!response.ok) {
397
+ const errorText = await response.text();
398
+ console.error(`[Reddit API] Delete failed: ${response.status} ${response.statusText}`);
399
+ console.error(`[Reddit API] Error response: ${errorText}`);
400
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
401
+ }
402
+ console.error(`[Reddit API] Successfully deleted ${fullThingId}`);
403
+ return true;
404
+ } catch (error) {
405
+ console.error(`[Reddit API] Delete exception:`, error);
406
+ if (error instanceof Error && error.message.includes("HTTP")) throw error;
407
+ throw new Error(`Failed to delete content ${thingId}: ${error instanceof Error ? error.message : String(error)}`);
408
+ }
409
+ }
410
+ async deleteComment(thingId) {
411
+ const fullThingId = thingId.startsWith("t1_") ? thingId : `t1_${thingId}`;
412
+ return this.deletePost(fullThingId);
413
+ }
414
+ async editPost(thingId, newText) {
415
+ this.validateWriteAccess();
416
+ await this.enforceWriteRateLimit();
417
+ this.checkDuplicateContent(newText);
418
+ try {
419
+ var _json$json6;
420
+ const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
421
+ const params = new URLSearchParams();
422
+ params.append("thing_id", fullThingId);
423
+ params.append("text", newText);
424
+ params.append("api_type", "json");
425
+ const response = await this.makeRequest("/api/editusertext", {
426
+ method: "POST",
427
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
428
+ body: params.toString()
429
+ });
430
+ if (!response.ok) {
431
+ const errorText = await response.text();
432
+ console.error(`[Reddit API] Edit failed: ${response.status} ${response.statusText}`);
433
+ console.error(`[Reddit API] Error response: ${errorText}`);
434
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
435
+ }
436
+ const json = await response.json();
437
+ console.error(`[Reddit API] Edit response:`, JSON.stringify(json, null, 2));
438
+ if (((_json$json6 = json.json) === null || _json$json6 === void 0 ? void 0 : _json$json6.errors) && json.json.errors.length > 0) {
439
+ const errors = json.json.errors.map((e) => e.join(": ")).join(", ");
440
+ console.error(`[Reddit API] Edit errors: ${errors}`);
441
+ throw new Error(`Reddit API errors: ${errors}`);
442
+ }
443
+ console.error(`[Reddit API] Successfully edited ${fullThingId}`);
444
+ return true;
445
+ } catch (error) {
446
+ console.error(`[Reddit API] Edit exception:`, error);
447
+ if (error instanceof Error && error.message.includes("HTTP")) throw error;
448
+ throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`);
449
+ }
450
+ }
451
+ async editComment(thingId, newText) {
452
+ const fullThingId = thingId.startsWith("t1_") ? thingId : `t1_${thingId}`;
453
+ return this.editPost(fullThingId, newText);
454
+ }
455
+ async searchReddit(query, options = {}) {
456
+ try {
457
+ const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
458
+ const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
459
+ const params = new URLSearchParams({
460
+ q: query,
461
+ sort,
462
+ t: timeFilter,
463
+ limit: limit.toString(),
464
+ type,
465
+ ...subreddit && { restrict_sr: "true" }
466
+ });
467
+ const response = await this.makeRequest(`${endpoint}?${params}`);
468
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
469
+ return (await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => {
470
+ const post = child.data;
471
+ return {
472
+ id: post.id,
473
+ title: post.title,
474
+ author: post.author,
475
+ subreddit: post.subreddit,
476
+ selftext: post.selftext || "",
477
+ url: post.url,
478
+ score: post.score,
479
+ upvoteRatio: post.upvote_ratio,
480
+ numComments: post.num_comments,
481
+ createdUtc: post.created_utc,
482
+ over18: post.over_18,
483
+ spoiler: post.spoiler,
484
+ edited: !!post.edited,
485
+ isSelf: post.is_self,
486
+ linkFlairText: post.link_flair_text ?? void 0,
487
+ permalink: post.permalink
488
+ };
489
+ });
490
+ } catch {
491
+ throw new Error(`Failed to search Reddit for: ${query}`);
492
+ }
493
+ }
494
+ async getPostComments(postId, subreddit, options = {}) {
495
+ try {
496
+ var _json$;
497
+ const { sort = "best", limit = 100 } = options;
498
+ const params = new URLSearchParams({
499
+ sort,
500
+ limit: limit.toString()
501
+ });
502
+ const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
503
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
504
+ const json = await response.json();
505
+ const postData = json[0].data.children[0].data;
506
+ const post = {
507
+ id: postData.id,
508
+ title: postData.title,
509
+ author: postData.author,
510
+ subreddit: postData.subreddit,
511
+ selftext: postData.selftext || "",
512
+ url: postData.url,
513
+ score: postData.score,
514
+ upvoteRatio: postData.upvote_ratio,
515
+ numComments: postData.num_comments,
516
+ createdUtc: postData.created_utc,
517
+ over18: postData.over_18,
518
+ spoiler: postData.spoiler,
519
+ edited: !!postData.edited,
520
+ isSelf: postData.is_self,
521
+ linkFlairText: postData.link_flair_text ?? void 0,
522
+ permalink: postData.permalink
523
+ };
524
+ const comments = [];
525
+ const parseComments = (commentData, depth = 0) => {
526
+ for (const item of commentData) if (item.kind === "t1" && item.data.body) {
527
+ var _replies$data;
528
+ comments.push({
529
+ id: item.data.id,
530
+ author: item.data.author,
531
+ body: item.data.body,
532
+ score: item.data.score,
533
+ controversiality: item.data.controversiality,
534
+ subreddit: item.data.subreddit,
535
+ submissionTitle: post.title,
536
+ createdUtc: item.data.created_utc,
537
+ edited: !!item.data.edited,
538
+ isSubmitter: item.data.is_submitter,
539
+ permalink: item.data.permalink,
540
+ depth,
541
+ parentId: item.data.parent_id
542
+ });
543
+ const replies = item.data.replies;
544
+ if (replies && typeof replies !== "string" && ((_replies$data = replies.data) === null || _replies$data === void 0 ? void 0 : _replies$data.children)) parseComments(replies.data.children, depth + 1);
545
+ }
546
+ };
547
+ if ((_json$ = json[1]) === null || _json$ === void 0 || (_json$ = _json$.data) === null || _json$ === void 0 ? void 0 : _json$.children) parseComments(json[1].data.children);
548
+ return {
549
+ post,
550
+ comments
551
+ };
552
+ } catch {
553
+ throw new Error(`Failed to get comments for post ${postId}`);
554
+ }
555
+ }
556
+ async getUserPosts(username, options = {}) {
557
+ try {
558
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
559
+ const params = new URLSearchParams({
560
+ sort,
561
+ t: timeFilter,
562
+ limit: limit.toString()
563
+ });
564
+ const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
565
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
566
+ return (await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => {
567
+ const post = child.data;
568
+ return {
569
+ id: post.id,
570
+ title: post.title,
571
+ author: post.author,
572
+ subreddit: post.subreddit,
573
+ selftext: post.selftext || "",
574
+ url: post.url,
575
+ score: post.score,
576
+ upvoteRatio: post.upvote_ratio,
577
+ numComments: post.num_comments,
578
+ createdUtc: post.created_utc,
579
+ over18: post.over_18,
580
+ spoiler: post.spoiler,
581
+ edited: !!post.edited,
582
+ isSelf: post.is_self,
583
+ linkFlairText: post.link_flair_text ?? void 0,
584
+ permalink: post.permalink
585
+ };
586
+ });
587
+ } catch {
588
+ throw new Error(`Failed to get posts for user ${username}`);
589
+ }
590
+ }
591
+ async getUserComments(username, options = {}) {
592
+ try {
593
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
594
+ const params = new URLSearchParams({
595
+ sort,
596
+ t: timeFilter,
597
+ limit: limit.toString()
598
+ });
599
+ const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
600
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
601
+ return (await response.json()).data.children.filter((child) => child.kind === "t1").map((child) => {
602
+ const comment = child.data;
603
+ return {
604
+ id: comment.id,
605
+ author: comment.author,
606
+ body: comment.body ?? "",
607
+ score: comment.score,
608
+ controversiality: comment.controversiality,
609
+ subreddit: comment.subreddit,
610
+ submissionTitle: comment.link_title ?? "",
611
+ createdUtc: comment.created_utc,
612
+ edited: !!comment.edited,
613
+ isSubmitter: comment.is_submitter,
614
+ permalink: comment.permalink
615
+ };
616
+ });
617
+ } catch {
618
+ throw new Error(`Failed to get comments for user ${username}`);
619
+ }
620
+ }
530
621
  };
531
- var redditClient = null;
622
+ let redditClient = null;
532
623
  function initializeRedditClient(config) {
533
- redditClient = new RedditClient(config);
534
- return redditClient;
624
+ redditClient = new RedditClient(config);
625
+ return redditClient;
535
626
  }
536
627
  function getRedditClient() {
537
- return redditClient;
628
+ return redditClient;
538
629
  }
539
630
 
540
- // src/utils/formatters.ts
631
+ //#endregion
632
+ //#region src/utils/formatters.ts
541
633
  function formatTimestamp(timestamp) {
542
- try {
543
- const date = new Date(timestamp * 1e3);
544
- return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
545
- } catch {
546
- return String(timestamp);
547
- }
634
+ try {
635
+ return (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
636
+ } catch {
637
+ return String(timestamp);
638
+ }
548
639
  }
549
640
  function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
550
- const insights = [];
551
- if (karmaRatio > 5) {
552
- insights.push("Primarily a commenter, highly engaged in discussions");
553
- } else if (karmaRatio < 0.2) {
554
- insights.push("Content creator, focuses on sharing posts");
555
- } else {
556
- insights.push("Balanced participation in both posting and commenting");
557
- }
558
- if (accountAgeDays < 30) {
559
- insights.push("New user, still exploring Reddit");
560
- } else if (accountAgeDays > 365 * 5) {
561
- insights.push("Long-time Redditor with extensive platform experience");
562
- }
563
- if (isMod) {
564
- insights.push("Community leader who helps maintain subreddit quality");
565
- }
566
- return insights.join("\n - ");
641
+ const insights = [];
642
+ if (karmaRatio > 5) insights.push("Primarily a commenter, highly engaged in discussions");
643
+ else if (karmaRatio < .2) insights.push("Content creator, focuses on sharing posts");
644
+ else insights.push("Balanced participation in both posting and commenting");
645
+ if (accountAgeDays < 30) insights.push("New user, still exploring Reddit");
646
+ else if (accountAgeDays > 365 * 5) insights.push("Long-time Redditor with extensive platform experience");
647
+ if (isMod) insights.push("Community leader who helps maintain subreddit quality");
648
+ return insights.join("\n - ");
567
649
  }
568
650
  function analyzePostEngagement(score, ratio, numComments) {
569
- const insights = [];
570
- if (score > 1e3 && ratio > 0.95) {
571
- insights.push("Highly successful post with strong community approval");
572
- } else if (score > 100 && ratio > 0.8) {
573
- insights.push("Well-received post with good engagement");
574
- } else if (ratio < 0.5) {
575
- insights.push("Controversial post that sparked debate");
576
- }
577
- if (numComments > 100) {
578
- insights.push("Generated significant discussion");
579
- } else if (numComments > score * 0.5) {
580
- insights.push("Highly discussable content with active comment section");
581
- } else if (numComments === 0) {
582
- insights.push("Yet to receive community interaction");
583
- }
584
- return insights.join("\n - ");
651
+ const insights = [];
652
+ if (score > 1e3 && ratio > .95) insights.push("Highly successful post with strong community approval");
653
+ else if (score > 100 && ratio > .8) insights.push("Well-received post with good engagement");
654
+ else if (ratio < .5) insights.push("Controversial post that sparked debate");
655
+ if (numComments > 100) insights.push("Generated significant discussion");
656
+ else if (numComments > score * .5) insights.push("Highly discussable content with active comment section");
657
+ else if (numComments === 0) insights.push("Yet to receive community interaction");
658
+ return insights.join("\n - ");
585
659
  }
586
660
  function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
587
- const insights = [];
588
- if (subscribers > 1e6) {
589
- insights.push("Major subreddit with massive following");
590
- } else if (subscribers > 1e5) {
591
- insights.push("Well-established community");
592
- } else if (subscribers < 1e3) {
593
- insights.push("Niche community, potential for growth");
594
- }
595
- if (activeUsers) {
596
- const activityRatio = activeUsers / subscribers;
597
- if (activityRatio > 0.1) {
598
- insights.push("Highly active community with strong engagement");
599
- } else if (activityRatio < 0.01) {
600
- insights.push("Could benefit from more community engagement initiatives");
601
- }
602
- }
603
- if (ageDays > 365 * 5) {
604
- insights.push("Mature subreddit with established culture");
605
- } else if (ageDays < 90) {
606
- insights.push("New subreddit still forming its community");
607
- }
608
- return insights.join("\n - ");
661
+ const insights = [];
662
+ if (subscribers > 1e6) insights.push("Major subreddit with massive following");
663
+ else if (subscribers > 1e5) insights.push("Well-established community");
664
+ else if (subscribers < 1e3) insights.push("Niche community, potential for growth");
665
+ if (activeUsers) {
666
+ const activityRatio = activeUsers / subscribers;
667
+ if (activityRatio > .1) insights.push("Highly active community with strong engagement");
668
+ else if (activityRatio < .01) insights.push("Could benefit from more community engagement initiatives");
669
+ }
670
+ if (ageDays > 365 * 5) insights.push("Mature subreddit with established culture");
671
+ else if (ageDays < 90) insights.push("New subreddit still forming its community");
672
+ return insights.join("\n - ");
609
673
  }
610
674
  function getUserRecommendations(karmaRatio, isMod, accountAgeDays) {
611
- const recommendations = [];
612
- if (karmaRatio > 5) {
613
- recommendations.push("Consider creating more posts to share your expertise");
614
- } else if (karmaRatio < 0.2) {
615
- recommendations.push("Engage more in discussions to build community connections");
616
- }
617
- if (accountAgeDays < 30) {
618
- recommendations.push("Explore popular subreddits in your areas of interest");
619
- recommendations.push("Read community guidelines before posting");
620
- }
621
- if (isMod) {
622
- recommendations.push("Share moderation insights with other community leaders");
623
- }
624
- if (!recommendations.length) {
625
- recommendations.push("Maintain your balanced engagement across Reddit");
626
- }
627
- return recommendations.join("\n - ");
675
+ const recommendations = [];
676
+ if (karmaRatio > 5) recommendations.push("Consider creating more posts to share your expertise");
677
+ else if (karmaRatio < .2) recommendations.push("Engage more in discussions to build community connections");
678
+ if (accountAgeDays < 30) {
679
+ recommendations.push("Explore popular subreddits in your areas of interest");
680
+ recommendations.push("Read community guidelines before posting");
681
+ }
682
+ if (isMod) recommendations.push("Share moderation insights with other community leaders");
683
+ if (!recommendations.length) recommendations.push("Maintain your balanced engagement across Reddit");
684
+ return recommendations.join("\n - ");
628
685
  }
629
686
  function getBestEngagementTime(createdUtc) {
630
- const postHour = new Date(createdUtc * 1e3).getHours();
631
- if (14 <= postHour && postHour <= 18) {
632
- return "Posted during peak engagement hours (2 PM - 6 PM), good timing!";
633
- } else if (23 <= postHour || postHour <= 5) {
634
- return "Consider posting during more active hours (morning to evening)";
635
- } else {
636
- return "Posted during moderate activity hours, timing could be optimized";
637
- }
687
+ const postHour = (/* @__PURE__ */ new Date(createdUtc * 1e3)).getHours();
688
+ if (14 <= postHour && postHour <= 18) return "Posted during peak engagement hours (2 PM - 6 PM), good timing!";
689
+ else if (23 <= postHour || postHour <= 5) return "Consider posting during more active hours (morning to evening)";
690
+ else return "Posted during moderate activity hours, timing could be optimized";
638
691
  }
639
692
  function getSubredditEngagementTips(subreddit) {
640
- const tips = [];
641
- if (subreddit.subscribers > 1e6) {
642
- tips.push("Post during peak hours for maximum visibility");
643
- tips.push("Ensure content is highly polished due to high competition");
644
- } else if (subreddit.subscribers < 1e3) {
645
- tips.push("Engage actively to help grow the community");
646
- tips.push("Consider cross-posting to related larger subreddits");
647
- }
648
- if (subreddit.activeUserCount) {
649
- const activityRatio = subreddit.activeUserCount / subreddit.subscribers;
650
- if (activityRatio > 0.1) {
651
- tips.push("Quick responses recommended due to high activity");
652
- }
653
- }
654
- return tips.length ? tips.join("\n - ") : "Regular engagement recommended to maintain community presence";
693
+ const tips = [];
694
+ if (subreddit.subscribers > 1e6) {
695
+ tips.push("Post during peak hours for maximum visibility");
696
+ tips.push("Ensure content is highly polished due to high competition");
697
+ } else if (subreddit.subscribers < 1e3) {
698
+ tips.push("Engage actively to help grow the community");
699
+ tips.push("Consider cross-posting to related larger subreddits");
700
+ }
701
+ if (subreddit.activeUserCount) {
702
+ if (subreddit.activeUserCount / subreddit.subscribers > .1) tips.push("Quick responses recommended due to high activity");
703
+ }
704
+ return tips.length ? tips.join("\n - ") : "Regular engagement recommended to maintain community presence";
655
705
  }
656
706
  function formatUserInfo(user) {
657
- const status = [];
658
- if (user.isMod) status.push("Moderator");
659
- if (user.isGold) status.push("Reddit Gold Member");
660
- if (user.isEmployee) status.push("Reddit Employee");
661
- const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / (24 * 3600);
662
- const karmaRatio = user.commentKarma / (user.linkKarma || 1);
663
- return {
664
- username: user.name,
665
- karma: {
666
- commentKarma: user.commentKarma,
667
- postKarma: user.linkKarma,
668
- totalKarma: user.totalKarma
669
- },
670
- accountStatus: status.length ? status : ["Regular User"],
671
- accountCreated: formatTimestamp(user.createdUtc),
672
- profileUrl: user.profileUrl,
673
- activityAnalysis: analyzeUserActivity(karmaRatio, user.isMod, accountAgeDays),
674
- recommendations: getUserRecommendations(karmaRatio, user.isMod, accountAgeDays)
675
- };
707
+ const status = [];
708
+ if (user.isMod) status.push("Moderator");
709
+ if (user.isGold) status.push("Reddit Gold Member");
710
+ if (user.isEmployee) status.push("Reddit Employee");
711
+ const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / (24 * 3600);
712
+ const karmaRatio = user.commentKarma / (user.linkKarma || 1);
713
+ return {
714
+ username: user.name,
715
+ karma: {
716
+ commentKarma: user.commentKarma,
717
+ postKarma: user.linkKarma,
718
+ totalKarma: user.totalKarma
719
+ },
720
+ accountStatus: status.length ? status : ["Regular User"],
721
+ accountCreated: formatTimestamp(user.createdUtc),
722
+ profileUrl: user.profileUrl,
723
+ activityAnalysis: analyzeUserActivity(karmaRatio, user.isMod, accountAgeDays),
724
+ recommendations: getUserRecommendations(karmaRatio, user.isMod, accountAgeDays)
725
+ };
676
726
  }
677
727
  function formatPostInfo(post) {
678
- const contentType = post.isSelf ? "Text Post" : "Link Post";
679
- const content = post.isSelf ? post.selftext || "" : post.url || "";
680
- const flags = [];
681
- if (post.over18) flags.push("NSFW");
682
- if (post.spoiler) flags.push("Spoiler");
683
- if (post.edited) flags.push("Edited");
684
- return {
685
- title: post.title,
686
- type: contentType,
687
- content: content.length > 300 ? content.substring(0, 297) + "..." : content,
688
- author: post.author,
689
- subreddit: post.subreddit,
690
- stats: {
691
- score: post.score,
692
- upvoteRatio: post.upvoteRatio,
693
- comments: post.numComments
694
- },
695
- metadata: {
696
- posted: formatTimestamp(post.createdUtc),
697
- flags,
698
- flair: post.linkFlairText || "None"
699
- },
700
- links: {
701
- fullPost: `https://reddit.com${post.permalink}`,
702
- shortLink: `https://redd.it/${post.id}`
703
- },
704
- engagementAnalysis: analyzePostEngagement(post.score, post.upvoteRatio, post.numComments),
705
- bestTimeToEngage: getBestEngagementTime(post.createdUtc)
706
- };
728
+ const contentType = post.isSelf ? "Text Post" : "Link Post";
729
+ const content = post.isSelf ? post.selftext || "" : post.url || "";
730
+ const flags = [];
731
+ if (post.over18) flags.push("NSFW");
732
+ if (post.spoiler) flags.push("Spoiler");
733
+ if (post.edited) flags.push("Edited");
734
+ return {
735
+ title: post.title,
736
+ type: contentType,
737
+ content: content.length > 300 ? content.substring(0, 297) + "..." : content,
738
+ author: post.author,
739
+ subreddit: post.subreddit,
740
+ stats: {
741
+ score: post.score,
742
+ upvoteRatio: post.upvoteRatio,
743
+ comments: post.numComments
744
+ },
745
+ metadata: {
746
+ posted: formatTimestamp(post.createdUtc),
747
+ flags,
748
+ flair: post.linkFlairText || "None"
749
+ },
750
+ links: {
751
+ fullPost: `https://reddit.com${post.permalink}`,
752
+ shortLink: `https://redd.it/${post.id}`
753
+ },
754
+ engagementAnalysis: analyzePostEngagement(post.score, post.upvoteRatio, post.numComments),
755
+ bestTimeToEngage: getBestEngagementTime(post.createdUtc)
756
+ };
707
757
  }
708
758
  function formatSubredditInfo(subreddit) {
709
- const flags = [];
710
- if (subreddit.over18) flags.push("NSFW");
711
- if (subreddit.subredditType) flags.push(`Type: ${subreddit.subredditType}`);
712
- const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / (24 * 3600);
713
- return {
714
- name: subreddit.displayName,
715
- title: subreddit.title,
716
- stats: {
717
- subscribers: subreddit.subscribers,
718
- activeUsers: subreddit.activeUserCount !== void 0 ? subreddit.activeUserCount : "Unknown"
719
- },
720
- description: {
721
- short: subreddit.publicDescription,
722
- full: subreddit.description.length > 300 ? subreddit.description.substring(0, 297) + "..." : subreddit.description
723
- },
724
- metadata: {
725
- created: formatTimestamp(subreddit.createdUtc),
726
- flags: flags.length ? flags : ["None"]
727
- },
728
- links: {
729
- subreddit: `https://reddit.com${subreddit.url}`,
730
- wiki: `https://reddit.com/r/${subreddit.displayName}/wiki`
731
- },
732
- communityAnalysis: analyzeSubredditHealth(subreddit.subscribers, subreddit.activeUserCount, ageDays),
733
- engagementTips: getSubredditEngagementTips(subreddit)
734
- };
759
+ const flags = [];
760
+ if (subreddit.over18) flags.push("NSFW");
761
+ if (subreddit.subredditType) flags.push(`Type: ${subreddit.subredditType}`);
762
+ const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / (24 * 3600);
763
+ return {
764
+ name: subreddit.displayName,
765
+ title: subreddit.title,
766
+ stats: {
767
+ subscribers: subreddit.subscribers,
768
+ activeUsers: subreddit.activeUserCount !== void 0 ? subreddit.activeUserCount : "Unknown"
769
+ },
770
+ description: {
771
+ short: subreddit.publicDescription,
772
+ full: subreddit.description.length > 300 ? subreddit.description.substring(0, 297) + "..." : subreddit.description
773
+ },
774
+ metadata: {
775
+ created: formatTimestamp(subreddit.createdUtc),
776
+ flags: flags.length ? flags : ["None"]
777
+ },
778
+ links: {
779
+ subreddit: `https://reddit.com${subreddit.url}`,
780
+ wiki: `https://reddit.com/r/${subreddit.displayName}/wiki`
781
+ },
782
+ communityAnalysis: analyzeSubredditHealth(subreddit.subscribers, subreddit.activeUserCount, ageDays),
783
+ engagementTips: getSubredditEngagementTips(subreddit)
784
+ };
735
785
  }
736
786
 
737
- // src/index.ts
738
- var import_dotenv = __toESM(require("dotenv"));
739
- import_dotenv.default.config();
787
+ //#endregion
788
+ //#region src/index.ts
789
+ dotenv.default.config();
790
+ function validateUserAgent(userAgent, username) {
791
+ if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
792
+ console.error("[Warning] User-Agent does not follow Reddit's recommended format");
793
+ console.error("[Warning] Recommended: 'platform:app_id:version (by /u/username)'");
794
+ console.error("[Warning] Non-standard User-Agents may increase ban risk");
795
+ if (username) console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server:1.2.0 (by /u/${username})'`);
796
+ }
797
+ }
798
+ function buildUserAgent(customAgent, username) {
799
+ if (customAgent) {
800
+ validateUserAgent(customAgent, username);
801
+ return customAgent;
802
+ }
803
+ if (username) {
804
+ const autoAgent = `typescript:reddit-mcp-server:1.2.0 (by /u/${username})`;
805
+ console.error(`[Setup] Auto-generated User-Agent: ${autoAgent}`);
806
+ return autoAgent;
807
+ }
808
+ const fallbackAgent = "RedditMCPServer/1.2.0";
809
+ validateUserAgent(fallbackAgent);
810
+ return fallbackAgent;
811
+ }
812
+ function buildSafeModeConfig(safeMode) {
813
+ switch (safeMode) {
814
+ case "off": return {
815
+ enabled: false,
816
+ mode: "off",
817
+ writeDelayMs: 0,
818
+ duplicateCheck: false,
819
+ maxRecentHashes: 10
820
+ };
821
+ case "standard": return {
822
+ enabled: true,
823
+ mode: "standard",
824
+ writeDelayMs: 2e3,
825
+ duplicateCheck: true,
826
+ maxRecentHashes: 10
827
+ };
828
+ case "strict": return {
829
+ enabled: true,
830
+ mode: "strict",
831
+ writeDelayMs: 5e3,
832
+ duplicateCheck: true,
833
+ maxRecentHashes: 20
834
+ };
835
+ }
836
+ }
740
837
  async function setupRedditClient() {
741
- const clientId = process.env.REDDIT_CLIENT_ID;
742
- const clientSecret = process.env.REDDIT_CLIENT_SECRET;
743
- const userAgent = process.env.REDDIT_USER_AGENT || "RedditMCPServer/1.1.0";
744
- const username = process.env.REDDIT_USERNAME;
745
- const password = process.env.REDDIT_PASSWORD;
746
- if (!clientId || !clientSecret) {
747
- console.error(
748
- "[Error] Missing required Reddit API credentials. Please set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables."
749
- );
750
- process.exit(1);
751
- }
752
- try {
753
- initializeRedditClient({
754
- clientId,
755
- clientSecret,
756
- userAgent,
757
- username,
758
- password
759
- });
760
- console.error("[Setup] Reddit client initialized");
761
- if (username && password) {
762
- console.error(`[Setup] Authenticated as user: ${username}`);
763
- } else {
764
- console.error("[Setup] Running in read-only mode (no user authentication)");
765
- }
766
- } catch (error) {
767
- console.error("[Error] Failed to initialize Reddit client:", error);
768
- process.exit(1);
769
- }
838
+ const clientId = process.env.REDDIT_CLIENT_ID;
839
+ const clientSecret = process.env.REDDIT_CLIENT_SECRET;
840
+ const customUserAgent = process.env.REDDIT_USER_AGENT;
841
+ const username = process.env.REDDIT_USERNAME;
842
+ const password = process.env.REDDIT_PASSWORD;
843
+ const authMode = process.env.REDDIT_AUTH_MODE || "auto";
844
+ const safeMode = process.env.REDDIT_SAFE_MODE || "off";
845
+ if (![
846
+ "auto",
847
+ "authenticated",
848
+ "anonymous"
849
+ ].includes(authMode)) {
850
+ console.error(`[Error] Invalid REDDIT_AUTH_MODE: ${authMode}`);
851
+ console.error("[Error] Valid options are: auto, authenticated, anonymous");
852
+ process.exit(1);
853
+ }
854
+ if (![
855
+ "off",
856
+ "standard",
857
+ "strict"
858
+ ].includes(safeMode)) {
859
+ console.error(`[Error] Invalid REDDIT_SAFE_MODE: ${safeMode}`);
860
+ console.error("[Error] Valid options are: off, standard, strict");
861
+ process.exit(1);
862
+ }
863
+ if (authMode === "authenticated" && (!clientId || !clientSecret)) {
864
+ console.error("[Error] Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
865
+ process.exit(1);
866
+ }
867
+ const hasCredentials = !!(clientId && clientSecret);
868
+ const userAgent = buildUserAgent(customUserAgent, username);
869
+ const safeModeConfig = buildSafeModeConfig(safeMode);
870
+ try {
871
+ const client = initializeRedditClient({
872
+ clientId: clientId || "",
873
+ clientSecret: clientSecret || "",
874
+ userAgent,
875
+ username,
876
+ password,
877
+ authMode,
878
+ safeMode: safeModeConfig
879
+ });
880
+ console.error("[Setup] Reddit client initialized");
881
+ console.error(`[Setup] Authentication mode: ${authMode}`);
882
+ if (authMode === "anonymous" || !hasCredentials) {
883
+ console.error("[Setup] Using anonymous Reddit API (~10 req/min)");
884
+ console.error("[Setup] No authentication required - ready to use!");
885
+ } else {
886
+ console.error("[Setup] Testing Reddit API connection...");
887
+ if (!await client.checkAuthentication()) {
888
+ console.error("[Error] ✗ Failed to connect to Reddit API");
889
+ console.error("[Error] Please check your REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
890
+ process.exit(1);
891
+ }
892
+ console.error("[Setup] ✓ Reddit API connection successful");
893
+ console.error("[Setup] Using OAuth Reddit API (60-100 req/min)");
894
+ }
895
+ if (username && password) {
896
+ console.error(`[Setup] ✓ User authenticated as: ${username}`);
897
+ console.error("[Setup] Write operations enabled (posting, replying, editing, deleting)");
898
+ } else {
899
+ console.error("[Setup] Read-only mode (no user credentials)");
900
+ console.error("[Setup] For write operations, set REDDIT_USERNAME and REDDIT_PASSWORD");
901
+ }
902
+ if (safeModeConfig.enabled) {
903
+ console.error(`[Setup] ✓ Safe mode enabled: ${safeModeConfig.mode}`);
904
+ console.error(`[Setup] - Write delay: ${safeModeConfig.writeDelayMs}ms between operations`);
905
+ console.error(`[Setup] - Duplicate detection: enabled (tracking last ${safeModeConfig.maxRecentHashes} items)`);
906
+ } else {
907
+ console.error("[Setup] Safe mode: off (no write operation safeguards)");
908
+ console.error("[Setup] Consider enabling REDDIT_SAFE_MODE=standard for spam protection");
909
+ }
910
+ } catch (error) {
911
+ console.error("[Error] ✗ Reddit API connection failed:", error instanceof Error ? error.message : error);
912
+ console.error("[Error] Please verify your Reddit API credentials");
913
+ process.exit(1);
914
+ }
770
915
  }
771
- var server = new import_fastmcp.FastMCP({
772
- name: "reddit-mcp-server",
773
- version: "1.1.0",
774
- instructions: `A comprehensive Reddit MCP server that provides tools for interacting with Reddit API.
775
-
916
+ const server = new fastmcp.FastMCP({
917
+ name: "reddit-mcp-server",
918
+ version: "1.2.0",
919
+ instructions: `A comprehensive Reddit MCP server that provides tools for interacting with Reddit API.
920
+
776
921
  Available capabilities:
777
922
  - Fetch Reddit posts, comments, and user information
778
- - Get subreddit details and statistics
923
+ - Get subreddit details and statistics
779
924
  - Search Reddit content across posts and subreddits
780
925
  - Create posts and reply to posts/comments (with authentication)
926
+ - Edit your own posts and comments (with authentication)
927
+ - Delete your own posts and comments (with authentication)
781
928
  - Analyze engagement metrics and community insights
782
929
 
783
- For write operations (posting, replying), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
784
- // Optional OAuth configuration for HTTP transport
785
- ...process.env.OAUTH_ENABLED === "true" && {
786
- authenticate: async (request) => {
787
- const authHeader = request.headers.authorization;
788
- const expectedToken = process.env.OAUTH_TOKEN;
789
- if (!expectedToken) {
790
- const token2 = Array.from(
791
- { length: 32 },
792
- () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(Math.random() * 62))
793
- ).join("");
794
- console.log(`[Auth] Generated OAuth token: ${token2}`);
795
- throw new Response(
796
- JSON.stringify({
797
- error: "No OAuth token configured",
798
- generatedToken: token2
799
- }),
800
- {
801
- status: 401,
802
- headers: { "Content-Type": "application/json" }
803
- }
804
- );
805
- }
806
- if (!(authHeader == null ? void 0 : authHeader.startsWith("Bearer "))) {
807
- throw new Response(null, {
808
- status: 401,
809
- statusText: "Missing or invalid Authorization header"
810
- });
811
- }
812
- const token = authHeader.slice(7);
813
- if (token !== expectedToken) {
814
- throw new Response(null, {
815
- status: 403,
816
- statusText: "Invalid token"
817
- });
818
- }
819
- return { authenticated: true };
820
- }
821
- }
930
+ For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
931
+ ...process.env.OAUTH_ENABLED === "true" && { authenticate: async (request) => {
932
+ const authHeader = request.headers.authorization;
933
+ const expectedToken = process.env.OAUTH_TOKEN;
934
+ if (!expectedToken) {
935
+ const token = Array.from({ length: 32 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(Math.random() * 62))).join("");
936
+ console.log(`[Auth] Generated OAuth token: ${token}`);
937
+ throw new Response(JSON.stringify({
938
+ error: "No OAuth token configured",
939
+ generatedToken: token
940
+ }), {
941
+ status: 401,
942
+ headers: { "Content-Type": "application/json" }
943
+ });
944
+ }
945
+ if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer "))) throw new Response(null, {
946
+ status: 401,
947
+ statusText: "Missing or invalid Authorization header"
948
+ });
949
+ if (authHeader.slice(7) !== expectedToken) throw new Response(null, {
950
+ status: 403,
951
+ statusText: "Invalid token"
952
+ });
953
+ return { authenticated: true };
954
+ } }
822
955
  });
823
956
  server.addTool({
824
- name: "test_reddit_mcp_server",
825
- description: "Test the Reddit MCP Server connection and configuration",
826
- parameters: import_zod.z.object({}),
827
- execute: async () => {
828
- const client = getRedditClient();
829
- const hasAuth = client ? "\u2713" : "\u2717";
830
- const hasWriteAccess = process.env.REDDIT_USERNAME && process.env.REDDIT_PASSWORD ? "\u2713" : "\u2717";
831
- return `Reddit MCP Server Status:
832
- - Server: \u2713 Running
957
+ name: "test_reddit_mcp_server",
958
+ description: "Test the Reddit MCP Server connection and configuration",
959
+ parameters: zod.z.object({}),
960
+ execute: async () => {
961
+ const client = getRedditClient();
962
+ const hasAuth = client ? "✓" : "✗";
963
+ const hasWriteAccess = process.env.REDDIT_USERNAME && process.env.REDDIT_PASSWORD ? "✓" : "✗";
964
+ return `Reddit MCP Server Status:
965
+ - Server: ✓ Running
833
966
  - Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
834
- - Write Access: ${hasWriteAccess} ${hasWriteAccess === "\u2713" ? "Available" : "Read-only mode"}
835
- - Version: 1.1.0
967
+ - Write Access: ${hasWriteAccess} ${hasWriteAccess === "✓" ? "Available" : "Read-only mode"}
968
+ - Version: 1.2.0
836
969
 
837
970
  Ready to handle Reddit API requests!`;
838
- }
971
+ }
839
972
  });
840
973
  server.addTool({
841
- name: "get_user_info",
842
- description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
843
- parameters: import_zod.z.object({
844
- username: import_zod.z.string().describe("The Reddit username (without u/ prefix)")
845
- }),
846
- execute: async (args) => {
847
- const client = getRedditClient();
848
- if (!client) {
849
- throw new Error("Reddit client not initialized");
850
- }
851
- const user = await client.getUser(args.username);
852
- const formattedUser = formatUserInfo(user);
853
- return `# User Information: u/${formattedUser.username}
974
+ name: "get_user_info",
975
+ description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
976
+ parameters: zod.z.object({ username: zod.z.string().describe("The Reddit username (without u/ prefix)") }),
977
+ execute: async (args) => {
978
+ const client = getRedditClient();
979
+ if (!client) throw new Error("Reddit client not initialized");
980
+ const formattedUser = formatUserInfo(await client.getUser(args.username));
981
+ return `# User Information: u/${formattedUser.username}
854
982
 
855
983
  ## Profile Overview
856
984
  - Username: u/${formattedUser.username}
@@ -867,98 +995,109 @@ server.addTool({
867
995
 
868
996
  ## Recommendations
869
997
  - ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`;
870
- }
998
+ }
871
999
  });
872
1000
  server.addTool({
873
- name: "get_user_posts",
874
- description: "Get recent posts by a Reddit user with sorting and filtering options",
875
- parameters: import_zod.z.object({
876
- username: import_zod.z.string().describe("The Reddit username (without u/ prefix)"),
877
- sort: import_zod.z.enum(["new", "hot", "top"]).default("new").describe("Sort order for posts"),
878
- time_filter: import_zod.z.enum(["hour", "day", "week", "month", "year", "all"]).default("all").describe("Time filter for top posts"),
879
- limit: import_zod.z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
880
- }),
881
- execute: async (args) => {
882
- const client = getRedditClient();
883
- if (!client) {
884
- throw new Error("Reddit client not initialized");
885
- }
886
- const posts = await client.getUserPosts(args.username, {
887
- sort: args.sort,
888
- timeFilter: args.time_filter,
889
- limit: args.limit
890
- });
891
- if (posts.length === 0) {
892
- return `No posts found for u/${args.username} with the specified filters.`;
893
- }
894
- const postSummaries = posts.map((post, index) => {
895
- const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
896
- return `### ${index + 1}. ${post.title} ${flags.join(" ")}
1001
+ name: "get_user_posts",
1002
+ description: "Get recent posts by a Reddit user with sorting and filtering options",
1003
+ parameters: zod.z.object({
1004
+ username: zod.z.string().describe("The Reddit username (without u/ prefix)"),
1005
+ sort: zod.z.enum([
1006
+ "new",
1007
+ "hot",
1008
+ "top"
1009
+ ]).default("new").describe("Sort order for posts"),
1010
+ time_filter: zod.z.enum([
1011
+ "hour",
1012
+ "day",
1013
+ "week",
1014
+ "month",
1015
+ "year",
1016
+ "all"
1017
+ ]).default("all").describe("Time filter for top posts"),
1018
+ limit: zod.z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
1019
+ }),
1020
+ execute: async (args) => {
1021
+ const client = getRedditClient();
1022
+ if (!client) throw new Error("Reddit client not initialized");
1023
+ const posts = await client.getUserPosts(args.username, {
1024
+ sort: args.sort,
1025
+ timeFilter: args.time_filter,
1026
+ limit: args.limit
1027
+ });
1028
+ if (posts.length === 0) return `No posts found for u/${args.username} with the specified filters.`;
1029
+ const postSummaries = posts.map((post, index) => {
1030
+ const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
1031
+ return `### ${index + 1}. ${post.title} ${flags.join(" ")}
897
1032
  - Subreddit: r/${post.subreddit}
898
1033
  - Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
899
1034
  - Comments: ${post.numComments.toLocaleString()}
900
- - Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
1035
+ - Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
901
1036
  - Link: https://reddit.com${post.permalink}`;
902
- }).join("\n\n");
903
- return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
1037
+ }).join("\n\n");
1038
+ return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
904
1039
 
905
1040
  ${postSummaries}`;
906
- }
1041
+ }
907
1042
  });
908
1043
  server.addTool({
909
- name: "get_user_comments",
910
- description: "Get recent comments by a Reddit user with sorting and filtering options",
911
- parameters: import_zod.z.object({
912
- username: import_zod.z.string().describe("The Reddit username (without u/ prefix)"),
913
- sort: import_zod.z.enum(["new", "hot", "top"]).default("new").describe("Sort order for comments"),
914
- time_filter: import_zod.z.enum(["hour", "day", "week", "month", "year", "all"]).default("all").describe("Time filter for top comments"),
915
- limit: import_zod.z.number().min(1).max(100).default(10).describe("Number of comments to retrieve")
916
- }),
917
- execute: async (args) => {
918
- const client = getRedditClient();
919
- if (!client) {
920
- throw new Error("Reddit client not initialized");
921
- }
922
- const comments = await client.getUserComments(args.username, {
923
- sort: args.sort,
924
- timeFilter: args.time_filter,
925
- limit: args.limit
926
- });
927
- if (comments.length === 0) {
928
- return `No comments found for u/${args.username} with the specified filters.`;
929
- }
930
- const commentSummaries = comments.map((comment, index) => {
931
- const truncatedBody = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body;
932
- const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
933
- return `### ${index + 1}. Comment ${flags.join(" ")}
1044
+ name: "get_user_comments",
1045
+ description: "Get recent comments by a Reddit user with sorting and filtering options",
1046
+ parameters: zod.z.object({
1047
+ username: zod.z.string().describe("The Reddit username (without u/ prefix)"),
1048
+ sort: zod.z.enum([
1049
+ "new",
1050
+ "hot",
1051
+ "top"
1052
+ ]).default("new").describe("Sort order for comments"),
1053
+ time_filter: zod.z.enum([
1054
+ "hour",
1055
+ "day",
1056
+ "week",
1057
+ "month",
1058
+ "year",
1059
+ "all"
1060
+ ]).default("all").describe("Time filter for top comments"),
1061
+ limit: zod.z.number().min(1).max(100).default(10).describe("Number of comments to retrieve")
1062
+ }),
1063
+ execute: async (args) => {
1064
+ const client = getRedditClient();
1065
+ if (!client) throw new Error("Reddit client not initialized");
1066
+ const comments = await client.getUserComments(args.username, {
1067
+ sort: args.sort,
1068
+ timeFilter: args.time_filter,
1069
+ limit: args.limit
1070
+ });
1071
+ if (comments.length === 0) return `No comments found for u/${args.username} with the specified filters.`;
1072
+ const commentSummaries = comments.map((comment, index) => {
1073
+ const truncatedBody = comment.body.length > 300 ? comment.body.substring(0, 300) + "..." : comment.body;
1074
+ const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
1075
+ return `### ${index + 1}. Comment ${flags.join(" ")}
934
1076
  In r/${comment.subreddit} on "${comment.submissionTitle}"
935
1077
 
936
1078
  > ${truncatedBody}
937
1079
 
938
1080
  - Score: ${comment.score.toLocaleString()}
939
- - Posted: ${new Date(comment.createdUtc * 1e3).toLocaleString()}
1081
+ - Posted: ${(/* @__PURE__ */ new Date(comment.createdUtc * 1e3)).toLocaleString()}
940
1082
  - Link: https://reddit.com${comment.permalink}`;
941
- }).join("\n\n");
942
- return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
1083
+ }).join("\n\n");
1084
+ return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
943
1085
 
944
1086
  ${commentSummaries}`;
945
- }
1087
+ }
946
1088
  });
947
1089
  server.addTool({
948
- name: "get_reddit_post",
949
- description: "Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
950
- parameters: import_zod.z.object({
951
- subreddit: import_zod.z.string().describe("The subreddit name (without r/ prefix)"),
952
- post_id: import_zod.z.string().describe("The Reddit post ID")
953
- }),
954
- execute: async (args) => {
955
- const client = getRedditClient();
956
- if (!client) {
957
- throw new Error("Reddit client not initialized");
958
- }
959
- const post = await client.getPost(args.post_id, args.subreddit);
960
- const formattedPost = formatPostInfo(post);
961
- return `# Post from r/${formattedPost.subreddit}
1090
+ name: "get_reddit_post",
1091
+ description: "Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
1092
+ parameters: zod.z.object({
1093
+ subreddit: zod.z.string().describe("The subreddit name (without r/ prefix)"),
1094
+ post_id: zod.z.string().describe("The Reddit post ID")
1095
+ }),
1096
+ execute: async (args) => {
1097
+ const client = getRedditClient();
1098
+ if (!client) throw new Error("Reddit client not initialized");
1099
+ const formattedPost = formatPostInfo(await client.getPost(args.post_id, args.subreddit));
1100
+ return `# Post from r/${formattedPost.subreddit}
962
1101
 
963
1102
  ## Post Details
964
1103
  - Title: ${formattedPost.title}
@@ -987,55 +1126,48 @@ ${formattedPost.content}
987
1126
 
988
1127
  ## Best Time to Engage
989
1128
  ${formattedPost.bestTimeToEngage}`;
990
- }
1129
+ }
991
1130
  });
992
1131
  server.addTool({
993
- name: "get_top_posts",
994
- description: "Get top posts from a subreddit or from the Reddit home feed",
995
- parameters: import_zod.z.object({
996
- subreddit: import_zod.z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
997
- time_filter: import_zod.z.enum(["hour", "day", "week", "month", "year", "all"]).default("week").describe("Time period for top posts"),
998
- limit: import_zod.z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
999
- }),
1000
- execute: async (args) => {
1001
- const client = getRedditClient();
1002
- if (!client) {
1003
- throw new Error("Reddit client not initialized");
1004
- }
1005
- const posts = await client.getTopPosts(args.subreddit || "", args.time_filter, args.limit);
1006
- if (posts.length === 0) {
1007
- const location2 = args.subreddit ? `r/${args.subreddit}` : "home feed";
1008
- return `No posts found in ${location2} for the specified time period.`;
1009
- }
1010
- const formattedPosts = posts.map(formatPostInfo);
1011
- const postSummaries = formattedPosts.map(
1012
- (post, index) => `### ${index + 1}. ${post.title}
1132
+ name: "get_top_posts",
1133
+ description: "Get top posts from a subreddit or from the Reddit home feed",
1134
+ parameters: zod.z.object({
1135
+ subreddit: zod.z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
1136
+ time_filter: zod.z.enum([
1137
+ "hour",
1138
+ "day",
1139
+ "week",
1140
+ "month",
1141
+ "year",
1142
+ "all"
1143
+ ]).default("week").describe("Time period for top posts"),
1144
+ limit: zod.z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
1145
+ }),
1146
+ execute: async (args) => {
1147
+ const client = getRedditClient();
1148
+ if (!client) throw new Error("Reddit client not initialized");
1149
+ const posts = await client.getTopPosts(args.subreddit || "", args.time_filter, args.limit);
1150
+ if (posts.length === 0) return `No posts found in ${args.subreddit ? `r/${args.subreddit}` : "home feed"} for the specified time period.`;
1151
+ const postSummaries = posts.map(formatPostInfo).map((post, index) => `### ${index + 1}. ${post.title}
1013
1152
  - Author: u/${post.author}
1014
1153
  - Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
1015
1154
  - Comments: ${post.stats.comments.toLocaleString()}
1016
1155
  - Posted: ${post.metadata.posted}
1017
- - Link: ${post.links.shortLink}`
1018
- ).join("\n\n");
1019
- const location = args.subreddit ? `r/${args.subreddit}` : "Home Feed";
1020
- return `# Top Posts from ${location} (${args.time_filter})
1156
+ - Link: ${post.links.shortLink}`).join("\n\n");
1157
+ return `# Top Posts from ${args.subreddit ? `r/${args.subreddit}` : "Home Feed"} (${args.time_filter})
1021
1158
 
1022
1159
  ${postSummaries}`;
1023
- }
1160
+ }
1024
1161
  });
1025
1162
  server.addTool({
1026
- name: "get_subreddit_info",
1027
- description: "Get detailed information about a subreddit including description, stats, and community analysis",
1028
- parameters: import_zod.z.object({
1029
- subreddit_name: import_zod.z.string().describe("The subreddit name (without r/ prefix)")
1030
- }),
1031
- execute: async (args) => {
1032
- const client = getRedditClient();
1033
- if (!client) {
1034
- throw new Error("Reddit client not initialized");
1035
- }
1036
- const subreddit = await client.getSubredditInfo(args.subreddit_name);
1037
- const formattedSubreddit = formatSubredditInfo(subreddit);
1038
- return `# Subreddit Information: r/${formattedSubreddit.name}
1163
+ name: "get_subreddit_info",
1164
+ description: "Get detailed information about a subreddit including description, stats, and community analysis",
1165
+ parameters: zod.z.object({ subreddit_name: zod.z.string().describe("The subreddit name (without r/ prefix)") }),
1166
+ execute: async (args) => {
1167
+ const client = getRedditClient();
1168
+ if (!client) throw new Error("Reddit client not initialized");
1169
+ const formattedSubreddit = formatSubredditInfo(await client.getSubredditInfo(args.subreddit_name));
1170
+ return `# Subreddit Information: r/${formattedSubreddit.name}
1039
1171
 
1040
1172
  ## Overview
1041
1173
  - Name: r/${formattedSubreddit.name}
@@ -1062,138 +1194,291 @@ ${formattedSubreddit.description.full}
1062
1194
 
1063
1195
  ## Engagement Tips
1064
1196
  - ${formattedSubreddit.engagementTips.replace(/\n - /g, "\n- ")}`;
1065
- }
1197
+ }
1066
1198
  });
1067
1199
  server.addTool({
1068
- name: "get_trending_subreddits",
1069
- description: "Get a list of currently trending subreddits",
1070
- parameters: import_zod.z.object({}),
1071
- execute: async () => {
1072
- const client = getRedditClient();
1073
- if (!client) {
1074
- throw new Error("Reddit client not initialized");
1075
- }
1076
- const trendingSubreddits = await client.getTrendingSubreddits();
1077
- return `# Trending Subreddits
1200
+ name: "get_trending_subreddits",
1201
+ description: "Get a list of currently trending subreddits",
1202
+ parameters: zod.z.object({}),
1203
+ execute: async () => {
1204
+ const client = getRedditClient();
1205
+ if (!client) throw new Error("Reddit client not initialized");
1206
+ return `# Trending Subreddits
1078
1207
 
1079
- ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`;
1080
- }
1208
+ ${(await client.getTrendingSubreddits()).map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`;
1209
+ }
1081
1210
  });
1082
1211
  server.addTool({
1083
- name: "search_reddit",
1084
- description: "Search Reddit for posts and content across subreddits",
1085
- parameters: import_zod.z.object({
1086
- query: import_zod.z.string().describe("Search query"),
1087
- subreddit: import_zod.z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
1088
- sort: import_zod.z.enum(["relevance", "hot", "top", "new", "comments"]).default("relevance").describe("Sort order"),
1089
- time_filter: import_zod.z.enum(["hour", "day", "week", "month", "year", "all"]).default("all").describe("Time filter"),
1090
- limit: import_zod.z.number().min(1).max(100).default(10).describe("Number of results"),
1091
- type: import_zod.z.enum(["link", "sr", "user"]).default("link").describe("Type of content to search")
1092
- }),
1093
- execute: async (args) => {
1094
- const client = getRedditClient();
1095
- if (!client) {
1096
- throw new Error("Reddit client not initialized");
1097
- }
1098
- if (!args.query || args.query.trim() === "") {
1099
- throw new Error("Search query cannot be empty");
1100
- }
1101
- const posts = await client.searchReddit(args.query, {
1102
- subreddit: args.subreddit,
1103
- sort: args.sort,
1104
- timeFilter: args.time_filter,
1105
- limit: args.limit,
1106
- type: args.type
1107
- });
1108
- if (posts.length === 0) {
1109
- const searchLocation2 = args.subreddit ? ` in r/${args.subreddit}` : "";
1110
- return `No results found for "${args.query}"${searchLocation2}.`;
1111
- }
1112
- const searchResults = posts.map((post, index) => {
1113
- const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
1114
- return `### ${index + 1}. ${post.title} ${flags.join(" ")}
1212
+ name: "search_reddit",
1213
+ description: "Search Reddit for posts and content across subreddits",
1214
+ parameters: zod.z.object({
1215
+ query: zod.z.string().describe("Search query"),
1216
+ subreddit: zod.z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
1217
+ sort: zod.z.enum([
1218
+ "relevance",
1219
+ "hot",
1220
+ "top",
1221
+ "new",
1222
+ "comments"
1223
+ ]).default("relevance").describe("Sort order"),
1224
+ time_filter: zod.z.enum([
1225
+ "hour",
1226
+ "day",
1227
+ "week",
1228
+ "month",
1229
+ "year",
1230
+ "all"
1231
+ ]).default("all").describe("Time filter"),
1232
+ limit: zod.z.number().min(1).max(100).default(10).describe("Number of results"),
1233
+ type: zod.z.enum([
1234
+ "link",
1235
+ "sr",
1236
+ "user"
1237
+ ]).default("link").describe("Type of content to search")
1238
+ }),
1239
+ execute: async (args) => {
1240
+ const client = getRedditClient();
1241
+ if (!client) throw new Error("Reddit client not initialized");
1242
+ if (!args.query || args.query.trim() === "") throw new Error("Search query cannot be empty");
1243
+ const posts = await client.searchReddit(args.query, {
1244
+ subreddit: args.subreddit,
1245
+ sort: args.sort,
1246
+ timeFilter: args.time_filter,
1247
+ limit: args.limit,
1248
+ type: args.type
1249
+ });
1250
+ if (posts.length === 0) {
1251
+ const searchLocation$1 = args.subreddit ? ` in r/${args.subreddit}` : "";
1252
+ return `No results found for "${args.query}"${searchLocation$1}.`;
1253
+ }
1254
+ const searchResults = posts.map((post, index) => {
1255
+ const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
1256
+ return `### ${index + 1}. ${post.title} ${flags.join(" ")}
1115
1257
  - Subreddit: r/${post.subreddit}
1116
1258
  - Author: u/${post.author}
1117
1259
  - Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
1118
1260
  - Comments: ${post.numComments.toLocaleString()}
1119
- - Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
1261
+ - Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
1120
1262
  - Link: https://reddit.com${post.permalink}`;
1121
- }).join("\n\n");
1122
- const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
1123
- return `# Reddit Search Results for: "${args.query}"${searchLocation}
1263
+ }).join("\n\n");
1264
+ const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
1265
+ return `# Reddit Search Results for: "${args.query}"${searchLocation}
1124
1266
 
1125
1267
  Sorted by: ${args.sort} | Time: ${args.time_filter} | Type: ${args.type}
1126
1268
 
1127
1269
  ${searchResults}`;
1128
- }
1270
+ }
1271
+ });
1272
+ server.addTool({
1273
+ name: "create_post",
1274
+ description: "Create a new post in a subreddit (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: Rapid posting or duplicate content may trigger Reddit's spam detection and result in account bans. Consider enabling REDDIT_SAFE_MODE=standard for protection.",
1275
+ parameters: zod.z.object({
1276
+ subreddit: zod.z.string().describe("The subreddit name (without r/ prefix)"),
1277
+ title: zod.z.string().describe("The post title"),
1278
+ content: zod.z.string().describe("The post content (text for self posts, URL for link posts)"),
1279
+ is_self: zod.z.boolean().default(true).describe("Whether this is a self post (text) or link post")
1280
+ }),
1281
+ execute: async (args) => {
1282
+ const client = getRedditClient();
1283
+ if (!client) throw new Error("Reddit client not initialized");
1284
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1285
+ const formattedPost = formatPostInfo(await client.createPost(args.subreddit, args.title, args.content, args.is_self));
1286
+ return `# Post Created Successfully
1287
+
1288
+ ## Post Details
1289
+ - Title: ${formattedPost.title}
1290
+ - Subreddit: r/${formattedPost.subreddit}
1291
+ - Type: ${formattedPost.type}
1292
+ - Link: ${formattedPost.links.fullPost}
1293
+
1294
+ Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
1295
+ }
1296
+ });
1297
+ server.addTool({
1298
+ name: "reply_to_post",
1299
+ description: "Post a reply to an existing Reddit post or comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: Rapid commenting or duplicate content may trigger Reddit's spam detection. Enable REDDIT_SAFE_MODE=standard for rate limiting and duplicate detection.",
1300
+ parameters: zod.z.object({
1301
+ post_id: zod.z.string().describe("The Reddit post ID (thing_id, e.g., t3_xxxxx for posts, t1_xxxxx for comments)"),
1302
+ content: zod.z.string().describe("The reply content")
1303
+ }),
1304
+ execute: async (args) => {
1305
+ const client = getRedditClient();
1306
+ if (!client) throw new Error("Reddit client not initialized");
1307
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1308
+ const comment = await client.replyToPost(args.post_id, args.content);
1309
+ return `# Reply Posted Successfully
1310
+
1311
+ ## Comment Details
1312
+ - Posted to: ${args.post_id}
1313
+ - Author: u/${process.env.REDDIT_USERNAME}
1314
+ - Comment ID: ${comment.id}
1315
+
1316
+ Your reply has been successfully posted.`;
1317
+ }
1129
1318
  });
1130
1319
  server.addTool({
1131
- name: "get_post_comments",
1132
- description: "Get comments from a specific Reddit post",
1133
- parameters: import_zod.z.object({
1134
- post_id: import_zod.z.string().describe("The Reddit post ID"),
1135
- subreddit: import_zod.z.string().describe("The subreddit name (without r/ prefix)"),
1136
- sort: import_zod.z.enum(["best", "top", "new", "controversial", "old", "qa"]).default("best").describe("Comment sort order"),
1137
- limit: import_zod.z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
1138
- }),
1139
- execute: async (args) => {
1140
- const client = getRedditClient();
1141
- if (!client) {
1142
- throw new Error("Reddit client not initialized");
1143
- }
1144
- if (!args.post_id || !args.subreddit) {
1145
- throw new Error("post_id and subreddit are required");
1146
- }
1147
- const data = await client.getPostComments(args.post_id, args.subreddit, {
1148
- sort: args.sort,
1149
- limit: args.limit
1150
- });
1151
- const post = data.post;
1152
- const comments = data.comments;
1153
- let response = `# Comments for: ${post.title}
1320
+ name: "delete_post",
1321
+ description: "Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1322
+ parameters: zod.z.object({ thing_id: zod.z.string().describe("The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing.") }),
1323
+ execute: async (args) => {
1324
+ const client = getRedditClient();
1325
+ if (!client) throw new Error("Reddit client not initialized");
1326
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1327
+ await client.deletePost(args.thing_id);
1328
+ return `# Post Deleted Successfully
1329
+
1330
+ The post ${args.thing_id} has been permanently deleted from Reddit.
1331
+
1332
+ **Note**: This action cannot be undone. The post content has been removed and cannot be recovered.`;
1333
+ }
1334
+ });
1335
+ server.addTool({
1336
+ name: "delete_comment",
1337
+ description: "Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1338
+ parameters: zod.z.object({ thing_id: zod.z.string().describe("The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing.") }),
1339
+ execute: async (args) => {
1340
+ const client = getRedditClient();
1341
+ if (!client) throw new Error("Reddit client not initialized");
1342
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1343
+ await client.deleteComment(args.thing_id);
1344
+ return `# Comment Deleted Successfully
1345
+
1346
+ The comment ${args.thing_id} has been permanently deleted from Reddit.
1347
+
1348
+ **Note**: This action cannot be undone. The comment content has been removed and cannot be recovered.`;
1349
+ }
1350
+ });
1351
+ server.addTool({
1352
+ name: "edit_post",
1353
+ description: "Edit your own Reddit post (self-text posts only, requires REDDIT_USERNAME and REDDIT_PASSWORD). You can only edit the text content of self posts, not titles or link posts. WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
1354
+ parameters: zod.z.object({
1355
+ thing_id: zod.z.string().describe("The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing."),
1356
+ new_text: zod.z.string().describe("The new text content for the post. Supports Reddit markdown formatting.")
1357
+ }),
1358
+ execute: async (args) => {
1359
+ const client = getRedditClient();
1360
+ if (!client) throw new Error("Reddit client not initialized");
1361
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1362
+ await client.editPost(args.thing_id, args.new_text);
1363
+ return `# Post Edited Successfully
1364
+
1365
+ The post ${args.thing_id} has been updated with your new content.
1366
+
1367
+ **Note**:
1368
+ - Only self (text) posts can be edited
1369
+ - Post titles cannot be edited
1370
+ - Link posts cannot be edited
1371
+ - An "edited" marker will appear on your post`;
1372
+ }
1373
+ });
1374
+ server.addTool({
1375
+ name: "edit_comment",
1376
+ description: "Edit your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). Update the text content of a comment you previously posted. WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
1377
+ parameters: zod.z.object({
1378
+ thing_id: zod.z.string().describe("The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing."),
1379
+ new_text: zod.z.string().describe("The new text content for the comment. Supports Reddit markdown formatting.")
1380
+ }),
1381
+ execute: async (args) => {
1382
+ const client = getRedditClient();
1383
+ if (!client) throw new Error("Reddit client not initialized");
1384
+ if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1385
+ await client.editComment(args.thing_id, args.new_text);
1386
+ return `# Comment Edited Successfully
1387
+
1388
+ The comment ${args.thing_id} has been updated with your new content.
1389
+
1390
+ **Note**: An "edited" marker will appear on your comment to show it has been modified.`;
1391
+ }
1392
+ });
1393
+ server.addTool({
1394
+ name: "get_post_comments",
1395
+ description: "Get comments from a specific Reddit post",
1396
+ parameters: zod.z.object({
1397
+ post_id: zod.z.string().describe("The Reddit post ID"),
1398
+ subreddit: zod.z.string().describe("The subreddit name (without r/ prefix)"),
1399
+ sort: zod.z.enum([
1400
+ "best",
1401
+ "top",
1402
+ "new",
1403
+ "controversial",
1404
+ "old",
1405
+ "qa"
1406
+ ]).default("best").describe("Comment sort order"),
1407
+ limit: zod.z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
1408
+ }),
1409
+ execute: async (args) => {
1410
+ const client = getRedditClient();
1411
+ if (!client) throw new Error("Reddit client not initialized");
1412
+ if (!args.post_id || !args.subreddit) throw new Error("post_id and subreddit are required");
1413
+ const data = await client.getPostComments(args.post_id, args.subreddit, {
1414
+ sort: args.sort,
1415
+ limit: args.limit
1416
+ });
1417
+ const post = data.post;
1418
+ const comments = data.comments;
1419
+ let response = `# Comments for: ${post.title}
1154
1420
 
1155
1421
  **Post by u/${post.author} in r/${post.subreddit}**
1156
1422
  - Score: ${post.score.toLocaleString()} | Comments: ${post.numComments.toLocaleString()}
1157
- - Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
1423
+ - Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
1158
1424
 
1159
1425
  ---
1160
1426
 
1161
1427
  `;
1162
- if (comments.length === 0) {
1163
- response += "No comments found for this post.";
1164
- return response;
1165
- }
1166
- const commentSummaries = comments.map((comment) => {
1167
- const indent = "\u2514\u2500".repeat(Math.min(comment.depth || 0, 3));
1168
- const authorBadge = comment.isSubmitter ? " **[OP]**" : "";
1169
- const editedBadge = comment.edited ? " *(edited)*" : "";
1170
- return `${indent} **u/${comment.author}**${authorBadge}${editedBadge} (${comment.score.toLocaleString()} points)
1428
+ if (comments.length === 0) {
1429
+ response += "No comments found for this post.";
1430
+ return response;
1431
+ }
1432
+ const commentSummaries = comments.map((comment) => {
1433
+ const indent = "└─".repeat(Math.min(comment.depth || 0, 3));
1434
+ const authorBadge = comment.isSubmitter ? " **[OP]**" : "";
1435
+ const editedBadge = comment.edited ? " *(edited)*" : "";
1436
+ return `${indent} **u/${comment.author}**${authorBadge}${editedBadge} (${comment.score.toLocaleString()} points)
1171
1437
 
1172
1438
  ${comment.body}
1173
1439
 
1174
1440
  ---`;
1175
- }).join("\n\n");
1176
- response += commentSummaries;
1177
- return response;
1178
- }
1441
+ }).join("\n\n");
1442
+ response += commentSummaries;
1443
+ return response;
1444
+ }
1179
1445
  });
1180
1446
  async function main() {
1181
- try {
1182
- await setupRedditClient();
1183
- await server.start({
1184
- transportType: "stdio"
1185
- });
1186
- } catch (error) {
1187
- console.error("[Error] Failed to start server:", error);
1188
- process.exit(1);
1189
- }
1447
+ try {
1448
+ await setupRedditClient();
1449
+ const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http";
1450
+ const port = parseInt(process.env.PORT || "3000");
1451
+ const host = process.env.HOST || "0.0.0.0";
1452
+ if (useHttp) {
1453
+ console.error(`[Setup] Starting HTTP server on ${host}:${port}`);
1454
+ await server.start({
1455
+ transportType: "httpStream",
1456
+ httpStream: {
1457
+ port,
1458
+ host,
1459
+ endpoint: "/mcp"
1460
+ }
1461
+ });
1462
+ console.error(`[Setup] HTTP server ready at http://${host}:${port}/mcp`);
1463
+ console.error(`[Setup] SSE endpoint available at http://${host}:${port}/sse`);
1464
+ } else {
1465
+ console.error("[Setup] Starting in stdio mode");
1466
+ await server.start({ transportType: "stdio" });
1467
+ }
1468
+ } catch (error) {
1469
+ console.error("[Error] Failed to start server:", error);
1470
+ process.exit(1);
1471
+ }
1190
1472
  }
1191
1473
  process.on("SIGINT", async () => {
1192
- console.error("[Shutdown] Shutting down Reddit MCP Server...");
1193
- process.exit(0);
1474
+ console.error("[Shutdown] Shutting down Reddit MCP Server...");
1475
+ process.exit(0);
1194
1476
  });
1195
1477
  process.on("SIGTERM", async () => {
1196
- console.error("[Shutdown] Shutting down Reddit MCP Server...");
1197
- process.exit(0);
1478
+ console.error("[Shutdown] Shutting down Reddit MCP Server...");
1479
+ process.exit(0);
1198
1480
  });
1199
1481
  main().catch(console.error);
1482
+
1483
+ //#endregion
1484
+ //# sourceMappingURL=index.js.map