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