reddit-mcp-server 1.2.1 → 1.4.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 +60 -28
- package/dist/bin.js +11 -14
- package/dist/bin.js.map +1 -1
- package/dist/index.js +153 -157
- package/dist/index.js.map +1 -1
- package/package.json +41 -30
- package/dist/chunk-kSYXY2_d.js +0 -34
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
crypto = require_chunk.__toESM(crypto);
|
|
6
|
-
let dotenv = require("dotenv");
|
|
7
|
-
dotenv = require_chunk.__toESM(dotenv);
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import { FastMCP } from "fastmcp";
|
|
4
|
+
import { z } from "zod";
|
|
8
5
|
|
|
9
6
|
//#region src/client/reddit-client.ts
|
|
10
7
|
var RedditClient = class {
|
|
@@ -21,7 +18,8 @@ var RedditClient = class {
|
|
|
21
18
|
hasCredentials;
|
|
22
19
|
safeMode;
|
|
23
20
|
lastWriteTime = 0;
|
|
24
|
-
|
|
21
|
+
recentContentRecords = [];
|
|
22
|
+
botDisclosure;
|
|
25
23
|
constructor(config) {
|
|
26
24
|
this.clientId = config.clientId;
|
|
27
25
|
this.clientSecret = config.clientSecret;
|
|
@@ -38,6 +36,10 @@ var RedditClient = class {
|
|
|
38
36
|
duplicateCheck: false,
|
|
39
37
|
maxRecentHashes: 10
|
|
40
38
|
};
|
|
39
|
+
this.botDisclosure = config.botDisclosure || {
|
|
40
|
+
enabled: false,
|
|
41
|
+
footer: ""
|
|
42
|
+
};
|
|
41
43
|
}
|
|
42
44
|
determineBaseUrl() {
|
|
43
45
|
switch (this.authMode) {
|
|
@@ -87,8 +89,8 @@ var RedditClient = class {
|
|
|
87
89
|
if (this.accessToken && now < this.tokenExpiry) return;
|
|
88
90
|
const authUrl = "https://www.reddit.com/api/v1/access_token";
|
|
89
91
|
const authData = new URLSearchParams();
|
|
90
|
-
const username = this
|
|
91
|
-
const password = this
|
|
92
|
+
const { username } = this;
|
|
93
|
+
const { password } = this;
|
|
92
94
|
if (!!(username && password)) {
|
|
93
95
|
authData.append("grant_type", "password");
|
|
94
96
|
authData.append("username", username);
|
|
@@ -114,7 +116,7 @@ var RedditClient = class {
|
|
|
114
116
|
this.authenticated = true;
|
|
115
117
|
} catch (error) {
|
|
116
118
|
if (error instanceof Error) throw error;
|
|
117
|
-
throw new Error("Failed to authenticate with Reddit API");
|
|
119
|
+
throw new Error("Failed to authenticate with Reddit API", { cause: error });
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
async checkAuthentication() {
|
|
@@ -143,23 +145,31 @@ var RedditClient = class {
|
|
|
143
145
|
this.lastWriteTime = Date.now();
|
|
144
146
|
}
|
|
145
147
|
hashContent(content) {
|
|
146
|
-
return crypto.
|
|
148
|
+
return crypto.createHash("sha256").update(content.trim().toLowerCase()).digest("hex");
|
|
147
149
|
}
|
|
148
|
-
checkDuplicateContent(content) {
|
|
150
|
+
checkDuplicateContent(content, subreddit) {
|
|
149
151
|
if (!this.safeMode.enabled || !this.safeMode.duplicateCheck) return;
|
|
150
152
|
const hash = this.hashContent(content);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const first = this.recentContentHashes.values().next().value;
|
|
155
|
-
if (first) this.recentContentHashes.delete(first);
|
|
153
|
+
for (const record of this.recentContentRecords) if (record.hash === hash) {
|
|
154
|
+
if (subreddit && record.subreddit && subreddit !== record.subreddit) throw new Error("Cross-subreddit duplicate detected. Reddit's Responsible Builder Policy prohibits posting identical or substantially similar content across multiple subreddits. Please create unique content for each subreddit.");
|
|
155
|
+
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.");
|
|
156
156
|
}
|
|
157
|
+
this.recentContentRecords.push({
|
|
158
|
+
hash,
|
|
159
|
+
subreddit: subreddit || "",
|
|
160
|
+
timestamp: Date.now()
|
|
161
|
+
});
|
|
162
|
+
while (this.recentContentRecords.length > this.safeMode.maxRecentHashes) this.recentContentRecords.shift();
|
|
163
|
+
}
|
|
164
|
+
appendBotDisclosure(content) {
|
|
165
|
+
if (!this.botDisclosure.enabled || !this.botDisclosure.footer) return content;
|
|
166
|
+
return `${content}${this.botDisclosure.footer}`;
|
|
157
167
|
}
|
|
158
168
|
async getUser(username) {
|
|
159
169
|
try {
|
|
160
170
|
const response = await this.makeRequest(`/user/${username}/about.json`);
|
|
161
171
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
162
|
-
const data =
|
|
172
|
+
const { data } = await response.json();
|
|
163
173
|
return {
|
|
164
174
|
name: data.name,
|
|
165
175
|
id: data.id,
|
|
@@ -180,7 +190,7 @@ var RedditClient = class {
|
|
|
180
190
|
try {
|
|
181
191
|
const response = await this.makeRequest(`/r/${subredditName}/about.json`);
|
|
182
192
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
183
|
-
const data =
|
|
193
|
+
const { data } = await response.json();
|
|
184
194
|
return {
|
|
185
195
|
displayName: data.display_name,
|
|
186
196
|
title: data.title,
|
|
@@ -278,7 +288,8 @@ var RedditClient = class {
|
|
|
278
288
|
async createPost(subreddit, title, content, isSelf = true) {
|
|
279
289
|
this.validateWriteAccess();
|
|
280
290
|
await this.enforceWriteRateLimit();
|
|
281
|
-
this.checkDuplicateContent(title + content);
|
|
291
|
+
this.checkDuplicateContent(title + content, subreddit);
|
|
292
|
+
const finalContent = isSelf ? this.appendBotDisclosure(content) : content;
|
|
282
293
|
try {
|
|
283
294
|
var _json$json, _json$json2, _json$json3;
|
|
284
295
|
const kind = isSelf ? "self" : "link";
|
|
@@ -286,37 +297,25 @@ var RedditClient = class {
|
|
|
286
297
|
params.append("sr", subreddit);
|
|
287
298
|
params.append("kind", kind);
|
|
288
299
|
params.append("title", title);
|
|
289
|
-
params.append(isSelf ? "text" : "url",
|
|
300
|
+
params.append(isSelf ? "text" : "url", finalContent);
|
|
290
301
|
params.append("api_type", "json");
|
|
291
302
|
const response = await this.makeRequest("/api/submit", {
|
|
292
303
|
method: "POST",
|
|
293
304
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
294
305
|
body: params.toString()
|
|
295
306
|
});
|
|
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
|
-
}
|
|
307
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
302
308
|
const json = await response.json();
|
|
303
|
-
console.error(`[Reddit API] Create post response:`, JSON.stringify(json, null, 2));
|
|
304
309
|
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
|
|
306
|
-
console.error(`[Reddit API] Post creation errors: ${errors}`);
|
|
310
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
307
311
|
throw new Error(`Reddit API errors: ${errors}`);
|
|
308
312
|
}
|
|
309
313
|
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}`);
|
|
314
|
+
if (!postId) throw new Error("No post ID returned from Reddit");
|
|
315
315
|
return await this.getPost(postId, subreddit);
|
|
316
316
|
} catch (error) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`);
|
|
317
|
+
if (error instanceof Error) throw error;
|
|
318
|
+
throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
320
319
|
}
|
|
321
320
|
}
|
|
322
321
|
async checkPostExists(postId) {
|
|
@@ -332,26 +331,22 @@ var RedditClient = class {
|
|
|
332
331
|
this.validateWriteAccess();
|
|
333
332
|
await this.enforceWriteRateLimit();
|
|
334
333
|
this.checkDuplicateContent(content);
|
|
334
|
+
const finalContent = this.appendBotDisclosure(content);
|
|
335
335
|
try {
|
|
336
336
|
var _json$json4, _json$json5;
|
|
337
|
-
|
|
337
|
+
const fullThingId = postId.startsWith("t3_") || postId.startsWith("t1_") ? postId : `t3_${postId}`;
|
|
338
|
+
if (!postId.startsWith("t1_") && !await this.checkPostExists(postId.replace(/^t3_/, ""))) throw new Error(`Post with ID ${postId} does not exist or is not accessible`);
|
|
338
339
|
const params = new URLSearchParams();
|
|
339
|
-
params.append("thing_id",
|
|
340
|
-
params.append("text",
|
|
340
|
+
params.append("thing_id", fullThingId);
|
|
341
|
+
params.append("text", finalContent);
|
|
341
342
|
params.append("api_type", "json");
|
|
342
343
|
const response = await this.makeRequest("/api/comment", {
|
|
343
344
|
method: "POST",
|
|
344
345
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
345
346
|
body: params.toString()
|
|
346
347
|
});
|
|
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
|
-
}
|
|
348
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
353
349
|
const json = await response.json();
|
|
354
|
-
console.error(`[Reddit API] Reply response:`, JSON.stringify(json, null, 2));
|
|
355
350
|
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
351
|
const commentData = json.json.data.things[0].data;
|
|
357
352
|
const author = this.username ?? "[unknown]";
|
|
@@ -369,17 +364,12 @@ var RedditClient = class {
|
|
|
369
364
|
permalink: commentData.permalink
|
|
370
365
|
};
|
|
371
366
|
} 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
|
|
373
|
-
console.error(`[Reddit API] Reply errors: ${errors}`);
|
|
367
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
374
368
|
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
|
-
}
|
|
369
|
+
} else throw new Error("Failed to parse reply response");
|
|
379
370
|
} catch (error) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
371
|
+
if (error instanceof Error) throw error;
|
|
372
|
+
throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
383
373
|
}
|
|
384
374
|
}
|
|
385
375
|
async deletePost(thingId) {
|
|
@@ -404,7 +394,7 @@ var RedditClient = class {
|
|
|
404
394
|
} catch (error) {
|
|
405
395
|
console.error(`[Reddit API] Delete exception:`, error);
|
|
406
396
|
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)}
|
|
397
|
+
throw new Error(`Failed to delete content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
408
398
|
}
|
|
409
399
|
}
|
|
410
400
|
async deleteComment(thingId) {
|
|
@@ -415,37 +405,29 @@ var RedditClient = class {
|
|
|
415
405
|
this.validateWriteAccess();
|
|
416
406
|
await this.enforceWriteRateLimit();
|
|
417
407
|
this.checkDuplicateContent(newText);
|
|
408
|
+
const finalText = this.appendBotDisclosure(newText);
|
|
418
409
|
try {
|
|
419
410
|
var _json$json6;
|
|
420
411
|
const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
|
|
421
412
|
const params = new URLSearchParams();
|
|
422
413
|
params.append("thing_id", fullThingId);
|
|
423
|
-
params.append("text",
|
|
414
|
+
params.append("text", finalText);
|
|
424
415
|
params.append("api_type", "json");
|
|
425
416
|
const response = await this.makeRequest("/api/editusertext", {
|
|
426
417
|
method: "POST",
|
|
427
418
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
428
419
|
body: params.toString()
|
|
429
420
|
});
|
|
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
|
-
}
|
|
421
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
436
422
|
const json = await response.json();
|
|
437
|
-
console.error(`[Reddit API] Edit response:`, JSON.stringify(json, null, 2));
|
|
438
423
|
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
|
|
440
|
-
console.error(`[Reddit API] Edit errors: ${errors}`);
|
|
424
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
441
425
|
throw new Error(`Reddit API errors: ${errors}`);
|
|
442
426
|
}
|
|
443
|
-
console.error(`[Reddit API] Successfully edited ${fullThingId}`);
|
|
444
427
|
return true;
|
|
445
428
|
} catch (error) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
429
|
+
if (error instanceof Error) throw error;
|
|
430
|
+
throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
449
431
|
}
|
|
450
432
|
}
|
|
451
433
|
async editComment(thingId, newText) {
|
|
@@ -540,7 +522,7 @@ var RedditClient = class {
|
|
|
540
522
|
depth,
|
|
541
523
|
parentId: item.data.parent_id
|
|
542
524
|
});
|
|
543
|
-
const replies = item.data
|
|
525
|
+
const { replies } = item.data;
|
|
544
526
|
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
527
|
}
|
|
546
528
|
};
|
|
@@ -734,7 +716,7 @@ function formatPostInfo(post) {
|
|
|
734
716
|
return {
|
|
735
717
|
title: post.title,
|
|
736
718
|
type: contentType,
|
|
737
|
-
content: content.length > 300 ? content.substring(0, 297)
|
|
719
|
+
content: content.length > 300 ? `${content.substring(0, 297)}...` : content,
|
|
738
720
|
author: post.author,
|
|
739
721
|
subreddit: post.subreddit,
|
|
740
722
|
stats: {
|
|
@@ -769,7 +751,7 @@ function formatSubredditInfo(subreddit) {
|
|
|
769
751
|
},
|
|
770
752
|
description: {
|
|
771
753
|
short: subreddit.publicDescription,
|
|
772
|
-
full: subreddit.description.length > 300 ? subreddit.description.substring(0, 297)
|
|
754
|
+
full: subreddit.description.length > 300 ? `${subreddit.description.substring(0, 297)}...` : subreddit.description
|
|
773
755
|
},
|
|
774
756
|
metadata: {
|
|
775
757
|
created: formatTimestamp(subreddit.createdUtc),
|
|
@@ -786,8 +768,8 @@ function formatSubredditInfo(subreddit) {
|
|
|
786
768
|
|
|
787
769
|
//#endregion
|
|
788
770
|
//#region src/index.ts
|
|
789
|
-
dotenv.
|
|
790
|
-
const VERSION = "1.
|
|
771
|
+
dotenv.config();
|
|
772
|
+
const VERSION = "1.4.0";
|
|
791
773
|
function validateUserAgent(userAgent, username) {
|
|
792
774
|
if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
|
|
793
775
|
console.error("[Warning] User-Agent does not follow Reddit's recommended format");
|
|
@@ -806,8 +788,8 @@ function buildUserAgent(customAgent, username) {
|
|
|
806
788
|
console.error(`[Setup] Auto-generated User-Agent: ${autoAgent}`);
|
|
807
789
|
return autoAgent;
|
|
808
790
|
}
|
|
809
|
-
const fallbackAgent = `
|
|
810
|
-
|
|
791
|
+
const fallbackAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/anonymous)`;
|
|
792
|
+
console.error("[Setup] No REDDIT_USERNAME set — using anonymous User-Agent. Set REDDIT_USERNAME for a personalized agent.");
|
|
811
793
|
return fallbackAgent;
|
|
812
794
|
}
|
|
813
795
|
function buildSafeModeConfig(safeMode) {
|
|
@@ -842,7 +824,7 @@ async function setupRedditClient() {
|
|
|
842
824
|
const username = process.env.REDDIT_USERNAME;
|
|
843
825
|
const password = process.env.REDDIT_PASSWORD;
|
|
844
826
|
const authMode = process.env.REDDIT_AUTH_MODE || "auto";
|
|
845
|
-
const safeMode = process.env.REDDIT_SAFE_MODE || "
|
|
827
|
+
const safeMode = process.env.REDDIT_SAFE_MODE || "standard";
|
|
846
828
|
if (" : ""
|
|
857
|
+
};
|
|
871
858
|
try {
|
|
872
859
|
const client = initializeRedditClient({
|
|
873
860
|
clientId: clientId || "",
|
|
@@ -876,7 +863,8 @@ async function setupRedditClient() {
|
|
|
876
863
|
username,
|
|
877
864
|
password,
|
|
878
865
|
authMode,
|
|
879
|
-
safeMode: safeModeConfig
|
|
866
|
+
safeMode: safeModeConfig,
|
|
867
|
+
botDisclosure: botDisclosureConfig
|
|
880
868
|
});
|
|
881
869
|
console.error("[Setup] Reddit client initialized");
|
|
882
870
|
console.error(`[Setup] Authentication mode: ${authMode}`);
|
|
@@ -904,9 +892,11 @@ async function setupRedditClient() {
|
|
|
904
892
|
console.error(`[Setup] ✓ Safe mode enabled: ${safeModeConfig.mode}`);
|
|
905
893
|
console.error(`[Setup] - Write delay: ${safeModeConfig.writeDelayMs}ms between operations`);
|
|
906
894
|
console.error(`[Setup] - Duplicate detection: enabled (tracking last ${safeModeConfig.maxRecentHashes} items)`);
|
|
907
|
-
} else
|
|
908
|
-
|
|
909
|
-
|
|
895
|
+
} else console.error("[Setup] Safe mode: off (explicitly disabled — ensure compliance with Reddit's Responsible Builder Policy)");
|
|
896
|
+
if (botDisclosureConfig.enabled) console.error("[Setup] ✓ Bot disclosure: enabled (automated content will include bot footer)");
|
|
897
|
+
else {
|
|
898
|
+
console.error("[Setup] Bot disclosure: off");
|
|
899
|
+
console.error("[Setup] For Reddit policy compliance, consider REDDIT_BOT_DISCLOSURE=auto");
|
|
910
900
|
}
|
|
911
901
|
} catch (error) {
|
|
912
902
|
console.error("[Error] ✗ Reddit API connection failed:", error instanceof Error ? error.message : error);
|
|
@@ -914,7 +904,9 @@ async function setupRedditClient() {
|
|
|
914
904
|
process.exit(1);
|
|
915
905
|
}
|
|
916
906
|
}
|
|
917
|
-
const
|
|
907
|
+
const oauthToken = process.env.OAUTH_TOKEN || crypto.randomBytes(32).toString("hex");
|
|
908
|
+
if (process.env.OAUTH_ENABLED === "true" && !process.env.OAUTH_TOKEN) console.error(`[Auth] Generated OAuth token: ${oauthToken}`);
|
|
909
|
+
const server = new FastMCP({
|
|
918
910
|
name: "reddit-mcp-server",
|
|
919
911
|
version: VERSION,
|
|
920
912
|
instructions: `A comprehensive Reddit MCP server that provides tools for interacting with Reddit API.
|
|
@@ -928,26 +920,29 @@ Available capabilities:
|
|
|
928
920
|
- Delete your own posts and comments (with authentication)
|
|
929
921
|
- Analyze engagement metrics and community insights
|
|
930
922
|
|
|
931
|
-
For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured
|
|
923
|
+
For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.
|
|
924
|
+
|
|
925
|
+
IMPORTANT - Reddit Responsible Builder Policy compliance:
|
|
926
|
+
- Data retrieved via these tools must NOT be used for AI model training without Reddit's written approval
|
|
927
|
+
- Data must NOT be sold, licensed, or commercially redistributed
|
|
928
|
+
- Do NOT attempt to de-anonymize or re-identify Reddit users
|
|
929
|
+
- Do NOT post identical or substantially similar content across multiple subreddits
|
|
930
|
+
- Do NOT use these tools to manipulate votes, karma, or circumvent Reddit safety mechanisms
|
|
931
|
+
- All bot-generated content must clearly disclose its automated nature
|
|
932
|
+
- Bots must NOT send private/direct messages without explicit user consent
|
|
933
|
+
For details: https://support.reddithelp.com/hc/en-us/articles/42728983564564-Responsible-Builder-Policy`,
|
|
932
934
|
...process.env.OAUTH_ENABLED === "true" && { authenticate: async (request) => {
|
|
933
935
|
const authHeader = request.headers.authorization;
|
|
934
|
-
const expectedToken = process.env.OAUTH_TOKEN;
|
|
935
|
-
if (!expectedToken) {
|
|
936
|
-
const token = Array.from({ length: 32 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(Math.random() * 62))).join("");
|
|
937
|
-
console.log(`[Auth] Generated OAuth token: ${token}`);
|
|
938
|
-
throw new Response(JSON.stringify({
|
|
939
|
-
error: "No OAuth token configured",
|
|
940
|
-
generatedToken: token
|
|
941
|
-
}), {
|
|
942
|
-
status: 401,
|
|
943
|
-
headers: { "Content-Type": "application/json" }
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
936
|
if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer "))) throw new Response(null, {
|
|
947
937
|
status: 401,
|
|
948
938
|
statusText: "Missing or invalid Authorization header"
|
|
949
939
|
});
|
|
950
|
-
|
|
940
|
+
const token = authHeader.slice(7);
|
|
941
|
+
const tokenBuffer = Buffer.from(token);
|
|
942
|
+
const expectedBuffer = Buffer.from(oauthToken);
|
|
943
|
+
const tokenHash = crypto.createHash("sha256").update(tokenBuffer).digest();
|
|
944
|
+
const expectedHash = crypto.createHash("sha256").update(expectedBuffer).digest();
|
|
945
|
+
if (!crypto.timingSafeEqual(tokenHash, expectedHash)) throw new Response(null, {
|
|
951
946
|
status: 403,
|
|
952
947
|
statusText: "Invalid token"
|
|
953
948
|
});
|
|
@@ -957,7 +952,7 @@ For write operations (posting, replying, editing, deleting), ensure REDDIT_USERN
|
|
|
957
952
|
server.addTool({
|
|
958
953
|
name: "test_reddit_mcp_server",
|
|
959
954
|
description: "Test the Reddit MCP Server connection and configuration",
|
|
960
|
-
parameters:
|
|
955
|
+
parameters: z.object({}),
|
|
961
956
|
execute: async () => {
|
|
962
957
|
const client = getRedditClient();
|
|
963
958
|
const hasAuth = client ? "✓" : "✗";
|
|
@@ -974,7 +969,7 @@ Ready to handle Reddit API requests!`;
|
|
|
974
969
|
server.addTool({
|
|
975
970
|
name: "get_user_info",
|
|
976
971
|
description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
|
|
977
|
-
parameters:
|
|
972
|
+
parameters: z.object({ username: z.string().describe("The Reddit username (without u/ prefix)") }),
|
|
978
973
|
execute: async (args) => {
|
|
979
974
|
const client = getRedditClient();
|
|
980
975
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -992,7 +987,7 @@ server.addTool({
|
|
|
992
987
|
- Profile URL: ${formattedUser.profileUrl}
|
|
993
988
|
|
|
994
989
|
## Activity Analysis
|
|
995
|
-
- ${formattedUser.activityAnalysis.replace(/\n
|
|
990
|
+
- ${formattedUser.activityAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
996
991
|
|
|
997
992
|
## Recommendations
|
|
998
993
|
- ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`;
|
|
@@ -1001,14 +996,14 @@ server.addTool({
|
|
|
1001
996
|
server.addTool({
|
|
1002
997
|
name: "get_user_posts",
|
|
1003
998
|
description: "Get recent posts by a Reddit user with sorting and filtering options",
|
|
1004
|
-
parameters:
|
|
1005
|
-
username:
|
|
1006
|
-
sort:
|
|
999
|
+
parameters: z.object({
|
|
1000
|
+
username: z.string().describe("The Reddit username (without u/ prefix)"),
|
|
1001
|
+
sort: z.enum([
|
|
1007
1002
|
"new",
|
|
1008
1003
|
"hot",
|
|
1009
1004
|
"top"
|
|
1010
1005
|
]).default("new").describe("Sort order for posts"),
|
|
1011
|
-
time_filter:
|
|
1006
|
+
time_filter: z.enum([
|
|
1012
1007
|
"hour",
|
|
1013
1008
|
"day",
|
|
1014
1009
|
"week",
|
|
@@ -1016,7 +1011,7 @@ server.addTool({
|
|
|
1016
1011
|
"year",
|
|
1017
1012
|
"all"
|
|
1018
1013
|
]).default("all").describe("Time filter for top posts"),
|
|
1019
|
-
limit:
|
|
1014
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
|
|
1020
1015
|
}),
|
|
1021
1016
|
execute: async (args) => {
|
|
1022
1017
|
const client = getRedditClient();
|
|
@@ -1044,14 +1039,14 @@ ${postSummaries}`;
|
|
|
1044
1039
|
server.addTool({
|
|
1045
1040
|
name: "get_user_comments",
|
|
1046
1041
|
description: "Get recent comments by a Reddit user with sorting and filtering options",
|
|
1047
|
-
parameters:
|
|
1048
|
-
username:
|
|
1049
|
-
sort:
|
|
1042
|
+
parameters: z.object({
|
|
1043
|
+
username: z.string().describe("The Reddit username (without u/ prefix)"),
|
|
1044
|
+
sort: z.enum([
|
|
1050
1045
|
"new",
|
|
1051
1046
|
"hot",
|
|
1052
1047
|
"top"
|
|
1053
1048
|
]).default("new").describe("Sort order for comments"),
|
|
1054
|
-
time_filter:
|
|
1049
|
+
time_filter: z.enum([
|
|
1055
1050
|
"hour",
|
|
1056
1051
|
"day",
|
|
1057
1052
|
"week",
|
|
@@ -1059,7 +1054,7 @@ server.addTool({
|
|
|
1059
1054
|
"year",
|
|
1060
1055
|
"all"
|
|
1061
1056
|
]).default("all").describe("Time filter for top comments"),
|
|
1062
|
-
limit:
|
|
1057
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of comments to retrieve")
|
|
1063
1058
|
}),
|
|
1064
1059
|
execute: async (args) => {
|
|
1065
1060
|
const client = getRedditClient();
|
|
@@ -1071,7 +1066,7 @@ server.addTool({
|
|
|
1071
1066
|
});
|
|
1072
1067
|
if (comments.length === 0) return `No comments found for u/${args.username} with the specified filters.`;
|
|
1073
1068
|
const commentSummaries = comments.map((comment, index) => {
|
|
1074
|
-
const truncatedBody = comment.body.length > 300 ? comment.body.substring(0, 300)
|
|
1069
|
+
const truncatedBody = comment.body.length > 300 ? `${comment.body.substring(0, 300)}...` : comment.body;
|
|
1075
1070
|
const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
|
|
1076
1071
|
return `### ${index + 1}. Comment ${flags.join(" ")}
|
|
1077
1072
|
In r/${comment.subreddit} on "${comment.submissionTitle}"
|
|
@@ -1090,9 +1085,9 @@ ${commentSummaries}`;
|
|
|
1090
1085
|
server.addTool({
|
|
1091
1086
|
name: "get_reddit_post",
|
|
1092
1087
|
description: "Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
|
|
1093
|
-
parameters:
|
|
1094
|
-
subreddit:
|
|
1095
|
-
post_id:
|
|
1088
|
+
parameters: z.object({
|
|
1089
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1090
|
+
post_id: z.string().describe("The Reddit post ID")
|
|
1096
1091
|
}),
|
|
1097
1092
|
execute: async (args) => {
|
|
1098
1093
|
const client = getRedditClient();
|
|
@@ -1123,7 +1118,7 @@ ${formattedPost.content}
|
|
|
1123
1118
|
- Short Link: ${formattedPost.links.shortLink}
|
|
1124
1119
|
|
|
1125
1120
|
## Engagement Analysis
|
|
1126
|
-
- ${formattedPost.engagementAnalysis.replace(/\n
|
|
1121
|
+
- ${formattedPost.engagementAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
1127
1122
|
|
|
1128
1123
|
## Best Time to Engage
|
|
1129
1124
|
${formattedPost.bestTimeToEngage}`;
|
|
@@ -1132,9 +1127,9 @@ ${formattedPost.bestTimeToEngage}`;
|
|
|
1132
1127
|
server.addTool({
|
|
1133
1128
|
name: "get_top_posts",
|
|
1134
1129
|
description: "Get top posts from a subreddit or from the Reddit home feed",
|
|
1135
|
-
parameters:
|
|
1136
|
-
subreddit:
|
|
1137
|
-
time_filter:
|
|
1130
|
+
parameters: z.object({
|
|
1131
|
+
subreddit: z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
|
|
1132
|
+
time_filter: z.enum([
|
|
1138
1133
|
"hour",
|
|
1139
1134
|
"day",
|
|
1140
1135
|
"week",
|
|
@@ -1142,7 +1137,7 @@ server.addTool({
|
|
|
1142
1137
|
"year",
|
|
1143
1138
|
"all"
|
|
1144
1139
|
]).default("week").describe("Time period for top posts"),
|
|
1145
|
-
limit:
|
|
1140
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
|
|
1146
1141
|
}),
|
|
1147
1142
|
execute: async (args) => {
|
|
1148
1143
|
const client = getRedditClient();
|
|
@@ -1163,7 +1158,7 @@ ${postSummaries}`;
|
|
|
1163
1158
|
server.addTool({
|
|
1164
1159
|
name: "get_subreddit_info",
|
|
1165
1160
|
description: "Get detailed information about a subreddit including description, stats, and community analysis",
|
|
1166
|
-
parameters:
|
|
1161
|
+
parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
|
|
1167
1162
|
execute: async (args) => {
|
|
1168
1163
|
const client = getRedditClient();
|
|
1169
1164
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1191,16 +1186,16 @@ ${formattedSubreddit.description.full}
|
|
|
1191
1186
|
- Wiki: ${formattedSubreddit.links.wiki}
|
|
1192
1187
|
|
|
1193
1188
|
## Community Analysis
|
|
1194
|
-
- ${formattedSubreddit.communityAnalysis.replace(/\n
|
|
1189
|
+
- ${formattedSubreddit.communityAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
1195
1190
|
|
|
1196
1191
|
## Engagement Tips
|
|
1197
|
-
- ${formattedSubreddit.engagementTips.replace(/\n
|
|
1192
|
+
- ${formattedSubreddit.engagementTips.replace(/\n {2}- /g, "\n- ")}`;
|
|
1198
1193
|
}
|
|
1199
1194
|
});
|
|
1200
1195
|
server.addTool({
|
|
1201
1196
|
name: "get_trending_subreddits",
|
|
1202
1197
|
description: "Get a list of currently trending subreddits",
|
|
1203
|
-
parameters:
|
|
1198
|
+
parameters: z.object({}),
|
|
1204
1199
|
execute: async () => {
|
|
1205
1200
|
const client = getRedditClient();
|
|
1206
1201
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1212,17 +1207,17 @@ ${(await client.getTrendingSubreddits()).map((subreddit, index) => `${index + 1}
|
|
|
1212
1207
|
server.addTool({
|
|
1213
1208
|
name: "search_reddit",
|
|
1214
1209
|
description: "Search Reddit for posts and content across subreddits",
|
|
1215
|
-
parameters:
|
|
1216
|
-
query:
|
|
1217
|
-
subreddit:
|
|
1218
|
-
sort:
|
|
1210
|
+
parameters: z.object({
|
|
1211
|
+
query: z.string().describe("Search query"),
|
|
1212
|
+
subreddit: z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
|
|
1213
|
+
sort: z.enum([
|
|
1219
1214
|
"relevance",
|
|
1220
1215
|
"hot",
|
|
1221
1216
|
"top",
|
|
1222
1217
|
"new",
|
|
1223
1218
|
"comments"
|
|
1224
1219
|
]).default("relevance").describe("Sort order"),
|
|
1225
|
-
time_filter:
|
|
1220
|
+
time_filter: z.enum([
|
|
1226
1221
|
"hour",
|
|
1227
1222
|
"day",
|
|
1228
1223
|
"week",
|
|
@@ -1230,8 +1225,8 @@ server.addTool({
|
|
|
1230
1225
|
"year",
|
|
1231
1226
|
"all"
|
|
1232
1227
|
]).default("all").describe("Time filter"),
|
|
1233
|
-
limit:
|
|
1234
|
-
type:
|
|
1228
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of results"),
|
|
1229
|
+
type: z.enum([
|
|
1235
1230
|
"link",
|
|
1236
1231
|
"sr",
|
|
1237
1232
|
"user"
|
|
@@ -1273,11 +1268,11 @@ ${searchResults}`;
|
|
|
1273
1268
|
server.addTool({
|
|
1274
1269
|
name: "create_post",
|
|
1275
1270
|
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.",
|
|
1276
|
-
parameters:
|
|
1277
|
-
subreddit:
|
|
1278
|
-
title:
|
|
1279
|
-
content:
|
|
1280
|
-
is_self:
|
|
1271
|
+
parameters: z.object({
|
|
1272
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1273
|
+
title: z.string().describe("The post title"),
|
|
1274
|
+
content: z.string().describe("The post content (text for self posts, URL for link posts)"),
|
|
1275
|
+
is_self: z.boolean().default(true).describe("Whether this is a self post (text) or link post")
|
|
1281
1276
|
}),
|
|
1282
1277
|
execute: async (args) => {
|
|
1283
1278
|
const client = getRedditClient();
|
|
@@ -1298,9 +1293,9 @@ Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
|
|
|
1298
1293
|
server.addTool({
|
|
1299
1294
|
name: "reply_to_post",
|
|
1300
1295
|
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.",
|
|
1301
|
-
parameters:
|
|
1302
|
-
post_id:
|
|
1303
|
-
content:
|
|
1296
|
+
parameters: z.object({
|
|
1297
|
+
post_id: z.string().describe("The Reddit post ID (thing_id, e.g., t3_xxxxx for posts, t1_xxxxx for comments)"),
|
|
1298
|
+
content: z.string().describe("The reply content")
|
|
1304
1299
|
}),
|
|
1305
1300
|
execute: async (args) => {
|
|
1306
1301
|
const client = getRedditClient();
|
|
@@ -1320,7 +1315,7 @@ Your reply has been successfully posted.`;
|
|
|
1320
1315
|
server.addTool({
|
|
1321
1316
|
name: "delete_post",
|
|
1322
1317
|
description: "Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
|
|
1323
|
-
parameters:
|
|
1318
|
+
parameters: z.object({ thing_id: 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.") }),
|
|
1324
1319
|
execute: async (args) => {
|
|
1325
1320
|
const client = getRedditClient();
|
|
1326
1321
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1336,7 +1331,7 @@ The post ${args.thing_id} has been permanently deleted from Reddit.
|
|
|
1336
1331
|
server.addTool({
|
|
1337
1332
|
name: "delete_comment",
|
|
1338
1333
|
description: "Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
|
|
1339
|
-
parameters:
|
|
1334
|
+
parameters: z.object({ thing_id: 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.") }),
|
|
1340
1335
|
execute: async (args) => {
|
|
1341
1336
|
const client = getRedditClient();
|
|
1342
1337
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1352,9 +1347,9 @@ The comment ${args.thing_id} has been permanently deleted from Reddit.
|
|
|
1352
1347
|
server.addTool({
|
|
1353
1348
|
name: "edit_post",
|
|
1354
1349
|
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.",
|
|
1355
|
-
parameters:
|
|
1356
|
-
thing_id:
|
|
1357
|
-
new_text:
|
|
1350
|
+
parameters: z.object({
|
|
1351
|
+
thing_id: 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."),
|
|
1352
|
+
new_text: z.string().describe("The new text content for the post. Supports Reddit markdown formatting.")
|
|
1358
1353
|
}),
|
|
1359
1354
|
execute: async (args) => {
|
|
1360
1355
|
const client = getRedditClient();
|
|
@@ -1375,9 +1370,9 @@ The post ${args.thing_id} has been updated with your new content.
|
|
|
1375
1370
|
server.addTool({
|
|
1376
1371
|
name: "edit_comment",
|
|
1377
1372
|
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.",
|
|
1378
|
-
parameters:
|
|
1379
|
-
thing_id:
|
|
1380
|
-
new_text:
|
|
1373
|
+
parameters: z.object({
|
|
1374
|
+
thing_id: 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."),
|
|
1375
|
+
new_text: z.string().describe("The new text content for the comment. Supports Reddit markdown formatting.")
|
|
1381
1376
|
}),
|
|
1382
1377
|
execute: async (args) => {
|
|
1383
1378
|
const client = getRedditClient();
|
|
@@ -1394,10 +1389,10 @@ The comment ${args.thing_id} has been updated with your new content.
|
|
|
1394
1389
|
server.addTool({
|
|
1395
1390
|
name: "get_post_comments",
|
|
1396
1391
|
description: "Get comments from a specific Reddit post",
|
|
1397
|
-
parameters:
|
|
1398
|
-
post_id:
|
|
1399
|
-
subreddit:
|
|
1400
|
-
sort:
|
|
1392
|
+
parameters: z.object({
|
|
1393
|
+
post_id: z.string().describe("The Reddit post ID"),
|
|
1394
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1395
|
+
sort: z.enum([
|
|
1401
1396
|
"best",
|
|
1402
1397
|
"top",
|
|
1403
1398
|
"new",
|
|
@@ -1405,7 +1400,7 @@ server.addTool({
|
|
|
1405
1400
|
"old",
|
|
1406
1401
|
"qa"
|
|
1407
1402
|
]).default("best").describe("Comment sort order"),
|
|
1408
|
-
limit:
|
|
1403
|
+
limit: z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
|
|
1409
1404
|
}),
|
|
1410
1405
|
execute: async (args) => {
|
|
1411
1406
|
const client = getRedditClient();
|
|
@@ -1415,8 +1410,8 @@ server.addTool({
|
|
|
1415
1410
|
sort: args.sort,
|
|
1416
1411
|
limit: args.limit
|
|
1417
1412
|
});
|
|
1418
|
-
const post = data
|
|
1419
|
-
const comments = data
|
|
1413
|
+
const { post } = data;
|
|
1414
|
+
const { comments } = data;
|
|
1420
1415
|
let response = `# Comments for: ${post.title}
|
|
1421
1416
|
|
|
1422
1417
|
**Post by u/${post.author} in r/${post.subreddit}**
|
|
@@ -1449,7 +1444,7 @@ async function main() {
|
|
|
1449
1444
|
await setupRedditClient();
|
|
1450
1445
|
const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http";
|
|
1451
1446
|
const port = parseInt(process.env.PORT || "3000");
|
|
1452
|
-
const host = process.env.HOST || "
|
|
1447
|
+
const host = process.env.HOST || "127.0.0.1";
|
|
1453
1448
|
if (useHttp) {
|
|
1454
1449
|
console.error(`[Setup] Starting HTTP server on ${host}:${port}`);
|
|
1455
1450
|
await server.start({
|
|
@@ -1482,4 +1477,5 @@ process.on("SIGTERM", async () => {
|
|
|
1482
1477
|
main().catch(console.error);
|
|
1483
1478
|
|
|
1484
1479
|
//#endregion
|
|
1480
|
+
export { };
|
|
1485
1481
|
//# sourceMappingURL=index.js.map
|