reddit-mcp-server 1.1.2 → 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/README.md +194 -367
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +21 -1573
- package/dist/bin.js.map +1 -0
- package/dist/chunk-kSYXY2_d.js +34 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1253 -1293
- package/dist/index.js.map +1 -0
- package/package.json +25 -29
package/dist/index.js
CHANGED
|
@@ -1,912 +1,922 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
//#region src/client/reddit-client.ts
|
|
30
10
|
var RedditClient = class {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
controversiality: comment.controversiality,
|
|
642
|
-
subreddit: comment.subreddit,
|
|
643
|
-
submissionTitle: comment.link_title || "",
|
|
644
|
-
createdUtc: comment.created_utc,
|
|
645
|
-
edited: !!comment.edited,
|
|
646
|
-
isSubmitter: comment.is_submitter,
|
|
647
|
-
permalink: comment.permalink
|
|
648
|
-
};
|
|
649
|
-
});
|
|
650
|
-
} catch {
|
|
651
|
-
throw new Error(`Failed to get comments for user ${username}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
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
|
+
}
|
|
654
621
|
};
|
|
655
|
-
|
|
622
|
+
let redditClient = null;
|
|
656
623
|
function initializeRedditClient(config) {
|
|
657
|
-
|
|
658
|
-
|
|
624
|
+
redditClient = new RedditClient(config);
|
|
625
|
+
return redditClient;
|
|
659
626
|
}
|
|
660
627
|
function getRedditClient() {
|
|
661
|
-
|
|
628
|
+
return redditClient;
|
|
662
629
|
}
|
|
663
630
|
|
|
664
|
-
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/utils/formatters.ts
|
|
665
633
|
function formatTimestamp(timestamp) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
634
|
+
try {
|
|
635
|
+
return (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
|
636
|
+
} catch {
|
|
637
|
+
return String(timestamp);
|
|
638
|
+
}
|
|
672
639
|
}
|
|
673
640
|
function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (accountAgeDays < 30) {
|
|
683
|
-
insights.push("New user, still exploring Reddit");
|
|
684
|
-
} else if (accountAgeDays > 365 * 5) {
|
|
685
|
-
insights.push("Long-time Redditor with extensive platform experience");
|
|
686
|
-
}
|
|
687
|
-
if (isMod) {
|
|
688
|
-
insights.push("Community leader who helps maintain subreddit quality");
|
|
689
|
-
}
|
|
690
|
-
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 - ");
|
|
691
649
|
}
|
|
692
650
|
function analyzePostEngagement(score, ratio, numComments) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (numComments > 100) {
|
|
702
|
-
insights.push("Generated significant discussion");
|
|
703
|
-
} else if (numComments > score * 0.5) {
|
|
704
|
-
insights.push("Highly discussable content with active comment section");
|
|
705
|
-
} else if (numComments === 0) {
|
|
706
|
-
insights.push("Yet to receive community interaction");
|
|
707
|
-
}
|
|
708
|
-
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 - ");
|
|
709
659
|
}
|
|
710
660
|
function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
} else if (activityRatio < 0.01) {
|
|
724
|
-
insights.push("Could benefit from more community engagement initiatives");
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (ageDays > 365 * 5) {
|
|
728
|
-
insights.push("Mature subreddit with established culture");
|
|
729
|
-
} else if (ageDays < 90) {
|
|
730
|
-
insights.push("New subreddit still forming its community");
|
|
731
|
-
}
|
|
732
|
-
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 - ");
|
|
733
673
|
}
|
|
734
674
|
function getUserRecommendations(karmaRatio, isMod, accountAgeDays) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (isMod) {
|
|
746
|
-
recommendations.push("Share moderation insights with other community leaders");
|
|
747
|
-
}
|
|
748
|
-
if (!recommendations.length) {
|
|
749
|
-
recommendations.push("Maintain your balanced engagement across Reddit");
|
|
750
|
-
}
|
|
751
|
-
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 - ");
|
|
752
685
|
}
|
|
753
686
|
function getBestEngagementTime(createdUtc) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return "Consider posting during more active hours (morning to evening)";
|
|
759
|
-
} else {
|
|
760
|
-
return "Posted during moderate activity hours, timing could be optimized";
|
|
761
|
-
}
|
|
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";
|
|
762
691
|
}
|
|
763
692
|
function getSubredditEngagementTips(subreddit) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
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";
|
|
779
705
|
}
|
|
780
706
|
function formatUserInfo(user) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
};
|
|
800
726
|
}
|
|
801
727
|
function formatPostInfo(post) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
};
|
|
831
757
|
}
|
|
832
758
|
function formatSubredditInfo(subreddit) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
+
};
|
|
859
785
|
}
|
|
860
786
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
+
}
|
|
864
837
|
async function setupRedditClient() {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
+
}
|
|
905
915
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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.
|
|
910
920
|
|
|
911
921
|
Available capabilities:
|
|
912
922
|
- Fetch Reddit posts, comments, and user information
|
|
@@ -918,76 +928,57 @@ Available capabilities:
|
|
|
918
928
|
- Analyze engagement metrics and community insights
|
|
919
929
|
|
|
920
930
|
For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
status: 401,
|
|
946
|
-
statusText: "Missing or invalid Authorization header"
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
const token = authHeader.slice(7);
|
|
950
|
-
if (token !== expectedToken) {
|
|
951
|
-
throw new Response(null, {
|
|
952
|
-
status: 403,
|
|
953
|
-
statusText: "Invalid token"
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
return { authenticated: true };
|
|
957
|
-
}
|
|
958
|
-
}
|
|
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
|
+
} }
|
|
959
955
|
});
|
|
960
956
|
server.addTool({
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
- Server:
|
|
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
|
|
970
966
|
- Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
|
|
971
|
-
- Write Access: ${hasWriteAccess} ${hasWriteAccess === "
|
|
972
|
-
- Version: 1.
|
|
967
|
+
- Write Access: ${hasWriteAccess} ${hasWriteAccess === "✓" ? "Available" : "Read-only mode"}
|
|
968
|
+
- Version: 1.2.0
|
|
973
969
|
|
|
974
970
|
Ready to handle Reddit API requests!`;
|
|
975
|
-
|
|
971
|
+
}
|
|
976
972
|
});
|
|
977
973
|
server.addTool({
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
throw new Error("Reddit client not initialized");
|
|
987
|
-
}
|
|
988
|
-
const user = await client.getUser(args.username);
|
|
989
|
-
const formattedUser = formatUserInfo(user);
|
|
990
|
-
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}
|
|
991
982
|
|
|
992
983
|
## Profile Overview
|
|
993
984
|
- Username: u/${formattedUser.username}
|
|
@@ -1004,98 +995,109 @@ server.addTool({
|
|
|
1004
995
|
|
|
1005
996
|
## Recommendations
|
|
1006
997
|
- ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`;
|
|
1007
|
-
|
|
998
|
+
}
|
|
1008
999
|
});
|
|
1009
1000
|
server.addTool({
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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(" ")}
|
|
1034
1032
|
- Subreddit: r/${post.subreddit}
|
|
1035
1033
|
- Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
1036
1034
|
- Comments: ${post.numComments.toLocaleString()}
|
|
1037
|
-
- Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
|
|
1035
|
+
- Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
|
|
1038
1036
|
- Link: https://reddit.com${post.permalink}`;
|
|
1039
|
-
|
|
1040
|
-
|
|
1037
|
+
}).join("\n\n");
|
|
1038
|
+
return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
|
|
1041
1039
|
|
|
1042
1040
|
${postSummaries}`;
|
|
1043
|
-
|
|
1041
|
+
}
|
|
1044
1042
|
});
|
|
1045
1043
|
server.addTool({
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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(" ")}
|
|
1071
1076
|
In r/${comment.subreddit} on "${comment.submissionTitle}"
|
|
1072
1077
|
|
|
1073
1078
|
> ${truncatedBody}
|
|
1074
1079
|
|
|
1075
1080
|
- Score: ${comment.score.toLocaleString()}
|
|
1076
|
-
- Posted: ${new Date(comment.createdUtc * 1e3).toLocaleString()}
|
|
1081
|
+
- Posted: ${(/* @__PURE__ */ new Date(comment.createdUtc * 1e3)).toLocaleString()}
|
|
1077
1082
|
- Link: https://reddit.com${comment.permalink}`;
|
|
1078
|
-
|
|
1079
|
-
|
|
1083
|
+
}).join("\n\n");
|
|
1084
|
+
return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
|
|
1080
1085
|
|
|
1081
1086
|
${commentSummaries}`;
|
|
1082
|
-
|
|
1087
|
+
}
|
|
1083
1088
|
});
|
|
1084
1089
|
server.addTool({
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const post = await client.getPost(args.post_id, args.subreddit);
|
|
1097
|
-
const formattedPost = formatPostInfo(post);
|
|
1098
|
-
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}
|
|
1099
1101
|
|
|
1100
1102
|
## Post Details
|
|
1101
1103
|
- Title: ${formattedPost.title}
|
|
@@ -1124,55 +1126,48 @@ ${formattedPost.content}
|
|
|
1124
1126
|
|
|
1125
1127
|
## Best Time to Engage
|
|
1126
1128
|
${formattedPost.bestTimeToEngage}`;
|
|
1127
|
-
|
|
1129
|
+
}
|
|
1128
1130
|
});
|
|
1129
1131
|
server.addTool({
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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}
|
|
1150
1152
|
- Author: u/${post.author}
|
|
1151
1153
|
- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
1152
1154
|
- Comments: ${post.stats.comments.toLocaleString()}
|
|
1153
1155
|
- Posted: ${post.metadata.posted}
|
|
1154
|
-
- Link: ${post.links.shortLink}`
|
|
1155
|
-
|
|
1156
|
-
const location = args.subreddit ? `r/${args.subreddit}` : "Home Feed";
|
|
1157
|
-
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})
|
|
1158
1158
|
|
|
1159
1159
|
${postSummaries}`;
|
|
1160
|
-
|
|
1160
|
+
}
|
|
1161
1161
|
});
|
|
1162
1162
|
server.addTool({
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
throw new Error("Reddit client not initialized");
|
|
1172
|
-
}
|
|
1173
|
-
const subreddit = await client.getSubredditInfo(args.subreddit_name);
|
|
1174
|
-
const formattedSubreddit = formatSubredditInfo(subreddit);
|
|
1175
|
-
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}
|
|
1176
1171
|
|
|
1177
1172
|
## Overview
|
|
1178
1173
|
- Name: r/${formattedSubreddit.name}
|
|
@@ -1199,93 +1194,96 @@ ${formattedSubreddit.description.full}
|
|
|
1199
1194
|
|
|
1200
1195
|
## Engagement Tips
|
|
1201
1196
|
- ${formattedSubreddit.engagementTips.replace(/\n - /g, "\n- ")}`;
|
|
1202
|
-
|
|
1197
|
+
}
|
|
1203
1198
|
});
|
|
1204
1199
|
server.addTool({
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
const trendingSubreddits = await client.getTrendingSubreddits();
|
|
1214
|
-
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
|
|
1215
1207
|
|
|
1216
|
-
${
|
|
1217
|
-
|
|
1208
|
+
${(await client.getTrendingSubreddits()).map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`;
|
|
1209
|
+
}
|
|
1218
1210
|
});
|
|
1219
1211
|
server.addTool({
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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(" ")}
|
|
1252
1257
|
- Subreddit: r/${post.subreddit}
|
|
1253
1258
|
- Author: u/${post.author}
|
|
1254
1259
|
- Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
1255
1260
|
- Comments: ${post.numComments.toLocaleString()}
|
|
1256
|
-
- Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
|
|
1261
|
+
- Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
|
|
1257
1262
|
- Link: https://reddit.com${post.permalink}`;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1263
|
+
}).join("\n\n");
|
|
1264
|
+
const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
|
|
1265
|
+
return `# Reddit Search Results for: "${args.query}"${searchLocation}
|
|
1261
1266
|
|
|
1262
1267
|
Sorted by: ${args.sort} | Time: ${args.time_filter} | Type: ${args.type}
|
|
1263
1268
|
|
|
1264
1269
|
${searchResults}`;
|
|
1265
|
-
|
|
1270
|
+
}
|
|
1266
1271
|
});
|
|
1267
1272
|
server.addTool({
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
throw new Error(
|
|
1283
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
const post = await client.createPost(args.subreddit, args.title, args.content, args.is_self);
|
|
1287
|
-
const formattedPost = formatPostInfo(post);
|
|
1288
|
-
return `# Post Created Successfully
|
|
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
|
|
1289
1287
|
|
|
1290
1288
|
## Post Details
|
|
1291
1289
|
- Title: ${formattedPost.title}
|
|
@@ -1294,27 +1292,21 @@ server.addTool({
|
|
|
1294
1292
|
- Link: ${formattedPost.links.fullPost}
|
|
1295
1293
|
|
|
1296
1294
|
Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
|
|
1297
|
-
|
|
1295
|
+
}
|
|
1298
1296
|
});
|
|
1299
1297
|
server.addTool({
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
throw new Error(
|
|
1313
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
const comment = await client.replyToPost(args.post_id, args.content);
|
|
1317
|
-
return `# Reply Posted Successfully
|
|
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
|
|
1318
1310
|
|
|
1319
1311
|
## Comment Details
|
|
1320
1312
|
- Posted to: ${args.post_id}
|
|
@@ -1322,81 +1314,53 @@ server.addTool({
|
|
|
1322
1314
|
- Comment ID: ${comment.id}
|
|
1323
1315
|
|
|
1324
1316
|
Your reply has been successfully posted.`;
|
|
1325
|
-
|
|
1317
|
+
}
|
|
1326
1318
|
});
|
|
1327
1319
|
server.addTool({
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (!client) {
|
|
1338
|
-
throw new Error("Reddit client not initialized");
|
|
1339
|
-
}
|
|
1340
|
-
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
|
|
1341
|
-
throw new Error(
|
|
1342
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
await client.deletePost(args.thing_id);
|
|
1346
|
-
return `# Post Deleted Successfully
|
|
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
|
|
1347
1329
|
|
|
1348
1330
|
The post ${args.thing_id} has been permanently deleted from Reddit.
|
|
1349
1331
|
|
|
1350
1332
|
**Note**: This action cannot be undone. The post content has been removed and cannot be recovered.`;
|
|
1351
|
-
|
|
1333
|
+
}
|
|
1352
1334
|
});
|
|
1353
1335
|
server.addTool({
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (!client) {
|
|
1364
|
-
throw new Error("Reddit client not initialized");
|
|
1365
|
-
}
|
|
1366
|
-
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
|
|
1367
|
-
throw new Error(
|
|
1368
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
await client.deleteComment(args.thing_id);
|
|
1372
|
-
return `# Comment Deleted Successfully
|
|
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
|
|
1373
1345
|
|
|
1374
1346
|
The comment ${args.thing_id} has been permanently deleted from Reddit.
|
|
1375
1347
|
|
|
1376
1348
|
**Note**: This action cannot be undone. The comment content has been removed and cannot be recovered.`;
|
|
1377
|
-
|
|
1349
|
+
}
|
|
1378
1350
|
});
|
|
1379
1351
|
server.addTool({
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
|
|
1394
|
-
throw new Error(
|
|
1395
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
await client.editPost(args.thing_id, args.new_text);
|
|
1399
|
-
return `# Post Edited Successfully
|
|
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
|
|
1400
1364
|
|
|
1401
1365
|
The post ${args.thing_id} has been updated with your new content.
|
|
1402
1366
|
|
|
@@ -1405,120 +1369,116 @@ The post ${args.thing_id} has been updated with your new content.
|
|
|
1405
1369
|
- Post titles cannot be edited
|
|
1406
1370
|
- Link posts cannot be edited
|
|
1407
1371
|
- An "edited" marker will appear on your post`;
|
|
1408
|
-
|
|
1372
|
+
}
|
|
1409
1373
|
});
|
|
1410
1374
|
server.addTool({
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) {
|
|
1425
|
-
throw new Error(
|
|
1426
|
-
"User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables."
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
await client.editComment(args.thing_id, args.new_text);
|
|
1430
|
-
return `# Comment Edited Successfully
|
|
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
|
|
1431
1387
|
|
|
1432
1388
|
The comment ${args.thing_id} has been updated with your new content.
|
|
1433
1389
|
|
|
1434
1390
|
**Note**: An "edited" marker will appear on your comment to show it has been modified.`;
|
|
1435
|
-
|
|
1391
|
+
}
|
|
1436
1392
|
});
|
|
1437
1393
|
server.addTool({
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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}
|
|
1461
1420
|
|
|
1462
1421
|
**Post by u/${post.author} in r/${post.subreddit}**
|
|
1463
1422
|
- Score: ${post.score.toLocaleString()} | Comments: ${post.numComments.toLocaleString()}
|
|
1464
|
-
- Posted: ${new Date(post.createdUtc * 1e3).toLocaleString()}
|
|
1423
|
+
- Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
|
|
1465
1424
|
|
|
1466
1425
|
---
|
|
1467
1426
|
|
|
1468
1427
|
`;
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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)
|
|
1478
1437
|
|
|
1479
1438
|
${comment.body}
|
|
1480
1439
|
|
|
1481
1440
|
---`;
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1441
|
+
}).join("\n\n");
|
|
1442
|
+
response += commentSummaries;
|
|
1443
|
+
return response;
|
|
1444
|
+
}
|
|
1486
1445
|
});
|
|
1487
1446
|
async function main() {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
process.exit(1);
|
|
1514
|
-
}
|
|
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
|
+
}
|
|
1515
1472
|
}
|
|
1516
1473
|
process.on("SIGINT", async () => {
|
|
1517
|
-
|
|
1518
|
-
|
|
1474
|
+
console.error("[Shutdown] Shutting down Reddit MCP Server...");
|
|
1475
|
+
process.exit(0);
|
|
1519
1476
|
});
|
|
1520
1477
|
process.on("SIGTERM", async () => {
|
|
1521
|
-
|
|
1522
|
-
|
|
1478
|
+
console.error("[Shutdown] Shutting down Reddit MCP Server...");
|
|
1479
|
+
process.exit(0);
|
|
1523
1480
|
});
|
|
1524
1481
|
main().catch(console.error);
|
|
1482
|
+
|
|
1483
|
+
//#endregion
|
|
1484
|
+
//# sourceMappingURL=index.js.map
|