reddit-mcp-server 1.2.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +5 -12
- package/dist/bin.js.map +1 -1
- package/dist/index.js +109 -146
- package/dist/index.js.map +1 -1
- package/package.json +41 -29
- 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 {
|
|
@@ -87,8 +84,8 @@ var RedditClient = class {
|
|
|
87
84
|
if (this.accessToken && now < this.tokenExpiry) return;
|
|
88
85
|
const authUrl = "https://www.reddit.com/api/v1/access_token";
|
|
89
86
|
const authData = new URLSearchParams();
|
|
90
|
-
const username = this
|
|
91
|
-
const password = this
|
|
87
|
+
const { username } = this;
|
|
88
|
+
const { password } = this;
|
|
92
89
|
if (!!(username && password)) {
|
|
93
90
|
authData.append("grant_type", "password");
|
|
94
91
|
authData.append("username", username);
|
|
@@ -114,7 +111,7 @@ var RedditClient = class {
|
|
|
114
111
|
this.authenticated = true;
|
|
115
112
|
} catch (error) {
|
|
116
113
|
if (error instanceof Error) throw error;
|
|
117
|
-
throw new Error("Failed to authenticate with Reddit API");
|
|
114
|
+
throw new Error("Failed to authenticate with Reddit API", { cause: error });
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
async checkAuthentication() {
|
|
@@ -143,7 +140,7 @@ var RedditClient = class {
|
|
|
143
140
|
this.lastWriteTime = Date.now();
|
|
144
141
|
}
|
|
145
142
|
hashContent(content) {
|
|
146
|
-
return crypto.
|
|
143
|
+
return crypto.createHash("sha256").update(content.trim().toLowerCase()).digest("hex");
|
|
147
144
|
}
|
|
148
145
|
checkDuplicateContent(content) {
|
|
149
146
|
if (!this.safeMode.enabled || !this.safeMode.duplicateCheck) return;
|
|
@@ -159,7 +156,7 @@ var RedditClient = class {
|
|
|
159
156
|
try {
|
|
160
157
|
const response = await this.makeRequest(`/user/${username}/about.json`);
|
|
161
158
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
162
|
-
const data =
|
|
159
|
+
const { data } = await response.json();
|
|
163
160
|
return {
|
|
164
161
|
name: data.name,
|
|
165
162
|
id: data.id,
|
|
@@ -180,7 +177,7 @@ var RedditClient = class {
|
|
|
180
177
|
try {
|
|
181
178
|
const response = await this.makeRequest(`/r/${subredditName}/about.json`);
|
|
182
179
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
183
|
-
const data =
|
|
180
|
+
const { data } = await response.json();
|
|
184
181
|
return {
|
|
185
182
|
displayName: data.display_name,
|
|
186
183
|
title: data.title,
|
|
@@ -293,30 +290,18 @@ var RedditClient = class {
|
|
|
293
290
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
294
291
|
body: params.toString()
|
|
295
292
|
});
|
|
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
|
-
}
|
|
293
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
302
294
|
const json = await response.json();
|
|
303
|
-
console.error(`[Reddit API] Create post response:`, JSON.stringify(json, null, 2));
|
|
304
295
|
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}`);
|
|
296
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
307
297
|
throw new Error(`Reddit API errors: ${errors}`);
|
|
308
298
|
}
|
|
309
299
|
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}`);
|
|
300
|
+
if (!postId) throw new Error("No post ID returned from Reddit");
|
|
315
301
|
return await this.getPost(postId, subreddit);
|
|
316
302
|
} catch (error) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`);
|
|
303
|
+
if (error instanceof Error) throw error;
|
|
304
|
+
throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
320
305
|
}
|
|
321
306
|
}
|
|
322
307
|
async checkPostExists(postId) {
|
|
@@ -334,9 +319,10 @@ var RedditClient = class {
|
|
|
334
319
|
this.checkDuplicateContent(content);
|
|
335
320
|
try {
|
|
336
321
|
var _json$json4, _json$json5;
|
|
337
|
-
|
|
322
|
+
const fullThingId = postId.startsWith("t3_") || postId.startsWith("t1_") ? postId : `t3_${postId}`;
|
|
323
|
+
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
324
|
const params = new URLSearchParams();
|
|
339
|
-
params.append("thing_id",
|
|
325
|
+
params.append("thing_id", fullThingId);
|
|
340
326
|
params.append("text", content);
|
|
341
327
|
params.append("api_type", "json");
|
|
342
328
|
const response = await this.makeRequest("/api/comment", {
|
|
@@ -344,14 +330,8 @@ var RedditClient = class {
|
|
|
344
330
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
345
331
|
body: params.toString()
|
|
346
332
|
});
|
|
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
|
-
}
|
|
333
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
353
334
|
const json = await response.json();
|
|
354
|
-
console.error(`[Reddit API] Reply response:`, JSON.stringify(json, null, 2));
|
|
355
335
|
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
336
|
const commentData = json.json.data.things[0].data;
|
|
357
337
|
const author = this.username ?? "[unknown]";
|
|
@@ -369,17 +349,12 @@ var RedditClient = class {
|
|
|
369
349
|
permalink: commentData.permalink
|
|
370
350
|
};
|
|
371
351
|
} 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}`);
|
|
352
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
374
353
|
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
|
-
}
|
|
354
|
+
} else throw new Error("Failed to parse reply response");
|
|
379
355
|
} catch (error) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
+
if (error instanceof Error) throw error;
|
|
357
|
+
throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
383
358
|
}
|
|
384
359
|
}
|
|
385
360
|
async deletePost(thingId) {
|
|
@@ -404,7 +379,7 @@ var RedditClient = class {
|
|
|
404
379
|
} catch (error) {
|
|
405
380
|
console.error(`[Reddit API] Delete exception:`, error);
|
|
406
381
|
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)}
|
|
382
|
+
throw new Error(`Failed to delete content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
408
383
|
}
|
|
409
384
|
}
|
|
410
385
|
async deleteComment(thingId) {
|
|
@@ -427,25 +402,16 @@ var RedditClient = class {
|
|
|
427
402
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
428
403
|
body: params.toString()
|
|
429
404
|
});
|
|
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
|
-
}
|
|
405
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
436
406
|
const json = await response.json();
|
|
437
|
-
console.error(`[Reddit API] Edit response:`, JSON.stringify(json, null, 2));
|
|
438
407
|
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}`);
|
|
408
|
+
const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
|
|
441
409
|
throw new Error(`Reddit API errors: ${errors}`);
|
|
442
410
|
}
|
|
443
|
-
console.error(`[Reddit API] Successfully edited ${fullThingId}`);
|
|
444
411
|
return true;
|
|
445
412
|
} catch (error) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
413
|
+
if (error instanceof Error) throw error;
|
|
414
|
+
throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
449
415
|
}
|
|
450
416
|
}
|
|
451
417
|
async editComment(thingId, newText) {
|
|
@@ -540,7 +506,7 @@ var RedditClient = class {
|
|
|
540
506
|
depth,
|
|
541
507
|
parentId: item.data.parent_id
|
|
542
508
|
});
|
|
543
|
-
const replies = item.data
|
|
509
|
+
const { replies } = item.data;
|
|
544
510
|
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
511
|
}
|
|
546
512
|
};
|
|
@@ -734,7 +700,7 @@ function formatPostInfo(post) {
|
|
|
734
700
|
return {
|
|
735
701
|
title: post.title,
|
|
736
702
|
type: contentType,
|
|
737
|
-
content: content.length > 300 ? content.substring(0, 297)
|
|
703
|
+
content: content.length > 300 ? `${content.substring(0, 297)}...` : content,
|
|
738
704
|
author: post.author,
|
|
739
705
|
subreddit: post.subreddit,
|
|
740
706
|
stats: {
|
|
@@ -769,7 +735,7 @@ function formatSubredditInfo(subreddit) {
|
|
|
769
735
|
},
|
|
770
736
|
description: {
|
|
771
737
|
short: subreddit.publicDescription,
|
|
772
|
-
full: subreddit.description.length > 300 ? subreddit.description.substring(0, 297)
|
|
738
|
+
full: subreddit.description.length > 300 ? `${subreddit.description.substring(0, 297)}...` : subreddit.description
|
|
773
739
|
},
|
|
774
740
|
metadata: {
|
|
775
741
|
created: formatTimestamp(subreddit.createdUtc),
|
|
@@ -786,13 +752,14 @@ function formatSubredditInfo(subreddit) {
|
|
|
786
752
|
|
|
787
753
|
//#endregion
|
|
788
754
|
//#region src/index.ts
|
|
789
|
-
dotenv.
|
|
755
|
+
dotenv.config();
|
|
756
|
+
const VERSION = "1.3.2";
|
|
790
757
|
function validateUserAgent(userAgent, username) {
|
|
791
758
|
if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
|
|
792
759
|
console.error("[Warning] User-Agent does not follow Reddit's recommended format");
|
|
793
760
|
console.error("[Warning] Recommended: 'platform:app_id:version (by /u/username)'");
|
|
794
761
|
console.error("[Warning] Non-standard User-Agents may increase ban risk");
|
|
795
|
-
if (username) console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server
|
|
762
|
+
if (username) console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server:${VERSION} (by /u/${username})'`);
|
|
796
763
|
}
|
|
797
764
|
}
|
|
798
765
|
function buildUserAgent(customAgent, username) {
|
|
@@ -801,12 +768,12 @@ function buildUserAgent(customAgent, username) {
|
|
|
801
768
|
return customAgent;
|
|
802
769
|
}
|
|
803
770
|
if (username) {
|
|
804
|
-
const autoAgent = `typescript:reddit-mcp-server
|
|
771
|
+
const autoAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/${username})`;
|
|
805
772
|
console.error(`[Setup] Auto-generated User-Agent: ${autoAgent}`);
|
|
806
773
|
return autoAgent;
|
|
807
774
|
}
|
|
808
|
-
const fallbackAgent =
|
|
809
|
-
|
|
775
|
+
const fallbackAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/anonymous)`;
|
|
776
|
+
console.error("[Setup] No REDDIT_USERNAME set — using anonymous User-Agent. Set REDDIT_USERNAME for a personalized agent.");
|
|
810
777
|
return fallbackAgent;
|
|
811
778
|
}
|
|
812
779
|
function buildSafeModeConfig(safeMode) {
|
|
@@ -913,9 +880,11 @@ async function setupRedditClient() {
|
|
|
913
880
|
process.exit(1);
|
|
914
881
|
}
|
|
915
882
|
}
|
|
916
|
-
const
|
|
883
|
+
const oauthToken = process.env.OAUTH_TOKEN || crypto.randomBytes(32).toString("hex");
|
|
884
|
+
if (process.env.OAUTH_ENABLED === "true" && !process.env.OAUTH_TOKEN) console.error(`[Auth] Generated OAuth token: ${oauthToken}`);
|
|
885
|
+
const server = new FastMCP({
|
|
917
886
|
name: "reddit-mcp-server",
|
|
918
|
-
version:
|
|
887
|
+
version: VERSION,
|
|
919
888
|
instructions: `A comprehensive Reddit MCP server that provides tools for interacting with Reddit API.
|
|
920
889
|
|
|
921
890
|
Available capabilities:
|
|
@@ -930,23 +899,16 @@ Available capabilities:
|
|
|
930
899
|
For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
|
|
931
900
|
...process.env.OAUTH_ENABLED === "true" && { authenticate: async (request) => {
|
|
932
901
|
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
902
|
if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer "))) throw new Response(null, {
|
|
946
903
|
status: 401,
|
|
947
904
|
statusText: "Missing or invalid Authorization header"
|
|
948
905
|
});
|
|
949
|
-
|
|
906
|
+
const token = authHeader.slice(7);
|
|
907
|
+
const tokenBuffer = Buffer.from(token);
|
|
908
|
+
const expectedBuffer = Buffer.from(oauthToken);
|
|
909
|
+
const tokenHash = crypto.createHash("sha256").update(tokenBuffer).digest();
|
|
910
|
+
const expectedHash = crypto.createHash("sha256").update(expectedBuffer).digest();
|
|
911
|
+
if (!crypto.timingSafeEqual(tokenHash, expectedHash)) throw new Response(null, {
|
|
950
912
|
status: 403,
|
|
951
913
|
statusText: "Invalid token"
|
|
952
914
|
});
|
|
@@ -956,16 +918,16 @@ For write operations (posting, replying, editing, deleting), ensure REDDIT_USERN
|
|
|
956
918
|
server.addTool({
|
|
957
919
|
name: "test_reddit_mcp_server",
|
|
958
920
|
description: "Test the Reddit MCP Server connection and configuration",
|
|
959
|
-
parameters:
|
|
921
|
+
parameters: z.object({}),
|
|
960
922
|
execute: async () => {
|
|
961
923
|
const client = getRedditClient();
|
|
962
924
|
const hasAuth = client ? "✓" : "✗";
|
|
963
925
|
const hasWriteAccess = process.env.REDDIT_USERNAME && process.env.REDDIT_PASSWORD ? "✓" : "✗";
|
|
964
926
|
return `Reddit MCP Server Status:
|
|
965
927
|
- Server: ✓ Running
|
|
966
|
-
- Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
|
|
928
|
+
- Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
|
|
967
929
|
- Write Access: ${hasWriteAccess} ${hasWriteAccess === "✓" ? "Available" : "Read-only mode"}
|
|
968
|
-
- Version:
|
|
930
|
+
- Version: ${VERSION}
|
|
969
931
|
|
|
970
932
|
Ready to handle Reddit API requests!`;
|
|
971
933
|
}
|
|
@@ -973,7 +935,7 @@ Ready to handle Reddit API requests!`;
|
|
|
973
935
|
server.addTool({
|
|
974
936
|
name: "get_user_info",
|
|
975
937
|
description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
|
|
976
|
-
parameters:
|
|
938
|
+
parameters: z.object({ username: z.string().describe("The Reddit username (without u/ prefix)") }),
|
|
977
939
|
execute: async (args) => {
|
|
978
940
|
const client = getRedditClient();
|
|
979
941
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -991,7 +953,7 @@ server.addTool({
|
|
|
991
953
|
- Profile URL: ${formattedUser.profileUrl}
|
|
992
954
|
|
|
993
955
|
## Activity Analysis
|
|
994
|
-
- ${formattedUser.activityAnalysis.replace(/\n
|
|
956
|
+
- ${formattedUser.activityAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
995
957
|
|
|
996
958
|
## Recommendations
|
|
997
959
|
- ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`;
|
|
@@ -1000,14 +962,14 @@ server.addTool({
|
|
|
1000
962
|
server.addTool({
|
|
1001
963
|
name: "get_user_posts",
|
|
1002
964
|
description: "Get recent posts by a Reddit user with sorting and filtering options",
|
|
1003
|
-
parameters:
|
|
1004
|
-
username:
|
|
1005
|
-
sort:
|
|
965
|
+
parameters: z.object({
|
|
966
|
+
username: z.string().describe("The Reddit username (without u/ prefix)"),
|
|
967
|
+
sort: z.enum([
|
|
1006
968
|
"new",
|
|
1007
969
|
"hot",
|
|
1008
970
|
"top"
|
|
1009
971
|
]).default("new").describe("Sort order for posts"),
|
|
1010
|
-
time_filter:
|
|
972
|
+
time_filter: z.enum([
|
|
1011
973
|
"hour",
|
|
1012
974
|
"day",
|
|
1013
975
|
"week",
|
|
@@ -1015,7 +977,7 @@ server.addTool({
|
|
|
1015
977
|
"year",
|
|
1016
978
|
"all"
|
|
1017
979
|
]).default("all").describe("Time filter for top posts"),
|
|
1018
|
-
limit:
|
|
980
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
|
|
1019
981
|
}),
|
|
1020
982
|
execute: async (args) => {
|
|
1021
983
|
const client = getRedditClient();
|
|
@@ -1043,14 +1005,14 @@ ${postSummaries}`;
|
|
|
1043
1005
|
server.addTool({
|
|
1044
1006
|
name: "get_user_comments",
|
|
1045
1007
|
description: "Get recent comments by a Reddit user with sorting and filtering options",
|
|
1046
|
-
parameters:
|
|
1047
|
-
username:
|
|
1048
|
-
sort:
|
|
1008
|
+
parameters: z.object({
|
|
1009
|
+
username: z.string().describe("The Reddit username (without u/ prefix)"),
|
|
1010
|
+
sort: z.enum([
|
|
1049
1011
|
"new",
|
|
1050
1012
|
"hot",
|
|
1051
1013
|
"top"
|
|
1052
1014
|
]).default("new").describe("Sort order for comments"),
|
|
1053
|
-
time_filter:
|
|
1015
|
+
time_filter: z.enum([
|
|
1054
1016
|
"hour",
|
|
1055
1017
|
"day",
|
|
1056
1018
|
"week",
|
|
@@ -1058,7 +1020,7 @@ server.addTool({
|
|
|
1058
1020
|
"year",
|
|
1059
1021
|
"all"
|
|
1060
1022
|
]).default("all").describe("Time filter for top comments"),
|
|
1061
|
-
limit:
|
|
1023
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of comments to retrieve")
|
|
1062
1024
|
}),
|
|
1063
1025
|
execute: async (args) => {
|
|
1064
1026
|
const client = getRedditClient();
|
|
@@ -1070,7 +1032,7 @@ server.addTool({
|
|
|
1070
1032
|
});
|
|
1071
1033
|
if (comments.length === 0) return `No comments found for u/${args.username} with the specified filters.`;
|
|
1072
1034
|
const commentSummaries = comments.map((comment, index) => {
|
|
1073
|
-
const truncatedBody = comment.body.length > 300 ? comment.body.substring(0, 300)
|
|
1035
|
+
const truncatedBody = comment.body.length > 300 ? `${comment.body.substring(0, 300)}...` : comment.body;
|
|
1074
1036
|
const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
|
|
1075
1037
|
return `### ${index + 1}. Comment ${flags.join(" ")}
|
|
1076
1038
|
In r/${comment.subreddit} on "${comment.submissionTitle}"
|
|
@@ -1089,9 +1051,9 @@ ${commentSummaries}`;
|
|
|
1089
1051
|
server.addTool({
|
|
1090
1052
|
name: "get_reddit_post",
|
|
1091
1053
|
description: "Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
|
|
1092
|
-
parameters:
|
|
1093
|
-
subreddit:
|
|
1094
|
-
post_id:
|
|
1054
|
+
parameters: z.object({
|
|
1055
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1056
|
+
post_id: z.string().describe("The Reddit post ID")
|
|
1095
1057
|
}),
|
|
1096
1058
|
execute: async (args) => {
|
|
1097
1059
|
const client = getRedditClient();
|
|
@@ -1122,7 +1084,7 @@ ${formattedPost.content}
|
|
|
1122
1084
|
- Short Link: ${formattedPost.links.shortLink}
|
|
1123
1085
|
|
|
1124
1086
|
## Engagement Analysis
|
|
1125
|
-
- ${formattedPost.engagementAnalysis.replace(/\n
|
|
1087
|
+
- ${formattedPost.engagementAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
1126
1088
|
|
|
1127
1089
|
## Best Time to Engage
|
|
1128
1090
|
${formattedPost.bestTimeToEngage}`;
|
|
@@ -1131,9 +1093,9 @@ ${formattedPost.bestTimeToEngage}`;
|
|
|
1131
1093
|
server.addTool({
|
|
1132
1094
|
name: "get_top_posts",
|
|
1133
1095
|
description: "Get top posts from a subreddit or from the Reddit home feed",
|
|
1134
|
-
parameters:
|
|
1135
|
-
subreddit:
|
|
1136
|
-
time_filter:
|
|
1096
|
+
parameters: z.object({
|
|
1097
|
+
subreddit: z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
|
|
1098
|
+
time_filter: z.enum([
|
|
1137
1099
|
"hour",
|
|
1138
1100
|
"day",
|
|
1139
1101
|
"week",
|
|
@@ -1141,7 +1103,7 @@ server.addTool({
|
|
|
1141
1103
|
"year",
|
|
1142
1104
|
"all"
|
|
1143
1105
|
]).default("week").describe("Time period for top posts"),
|
|
1144
|
-
limit:
|
|
1106
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
|
|
1145
1107
|
}),
|
|
1146
1108
|
execute: async (args) => {
|
|
1147
1109
|
const client = getRedditClient();
|
|
@@ -1162,7 +1124,7 @@ ${postSummaries}`;
|
|
|
1162
1124
|
server.addTool({
|
|
1163
1125
|
name: "get_subreddit_info",
|
|
1164
1126
|
description: "Get detailed information about a subreddit including description, stats, and community analysis",
|
|
1165
|
-
parameters:
|
|
1127
|
+
parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
|
|
1166
1128
|
execute: async (args) => {
|
|
1167
1129
|
const client = getRedditClient();
|
|
1168
1130
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1190,16 +1152,16 @@ ${formattedSubreddit.description.full}
|
|
|
1190
1152
|
- Wiki: ${formattedSubreddit.links.wiki}
|
|
1191
1153
|
|
|
1192
1154
|
## Community Analysis
|
|
1193
|
-
- ${formattedSubreddit.communityAnalysis.replace(/\n
|
|
1155
|
+
- ${formattedSubreddit.communityAnalysis.replace(/\n {2}- /g, "\n- ")}
|
|
1194
1156
|
|
|
1195
1157
|
## Engagement Tips
|
|
1196
|
-
- ${formattedSubreddit.engagementTips.replace(/\n
|
|
1158
|
+
- ${formattedSubreddit.engagementTips.replace(/\n {2}- /g, "\n- ")}`;
|
|
1197
1159
|
}
|
|
1198
1160
|
});
|
|
1199
1161
|
server.addTool({
|
|
1200
1162
|
name: "get_trending_subreddits",
|
|
1201
1163
|
description: "Get a list of currently trending subreddits",
|
|
1202
|
-
parameters:
|
|
1164
|
+
parameters: z.object({}),
|
|
1203
1165
|
execute: async () => {
|
|
1204
1166
|
const client = getRedditClient();
|
|
1205
1167
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1211,17 +1173,17 @@ ${(await client.getTrendingSubreddits()).map((subreddit, index) => `${index + 1}
|
|
|
1211
1173
|
server.addTool({
|
|
1212
1174
|
name: "search_reddit",
|
|
1213
1175
|
description: "Search Reddit for posts and content across subreddits",
|
|
1214
|
-
parameters:
|
|
1215
|
-
query:
|
|
1216
|
-
subreddit:
|
|
1217
|
-
sort:
|
|
1176
|
+
parameters: z.object({
|
|
1177
|
+
query: z.string().describe("Search query"),
|
|
1178
|
+
subreddit: z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
|
|
1179
|
+
sort: z.enum([
|
|
1218
1180
|
"relevance",
|
|
1219
1181
|
"hot",
|
|
1220
1182
|
"top",
|
|
1221
1183
|
"new",
|
|
1222
1184
|
"comments"
|
|
1223
1185
|
]).default("relevance").describe("Sort order"),
|
|
1224
|
-
time_filter:
|
|
1186
|
+
time_filter: z.enum([
|
|
1225
1187
|
"hour",
|
|
1226
1188
|
"day",
|
|
1227
1189
|
"week",
|
|
@@ -1229,8 +1191,8 @@ server.addTool({
|
|
|
1229
1191
|
"year",
|
|
1230
1192
|
"all"
|
|
1231
1193
|
]).default("all").describe("Time filter"),
|
|
1232
|
-
limit:
|
|
1233
|
-
type:
|
|
1194
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of results"),
|
|
1195
|
+
type: z.enum([
|
|
1234
1196
|
"link",
|
|
1235
1197
|
"sr",
|
|
1236
1198
|
"user"
|
|
@@ -1248,8 +1210,8 @@ server.addTool({
|
|
|
1248
1210
|
type: args.type
|
|
1249
1211
|
});
|
|
1250
1212
|
if (posts.length === 0) {
|
|
1251
|
-
const searchLocation
|
|
1252
|
-
return `No results found for "${args.query}"${searchLocation
|
|
1213
|
+
const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
|
|
1214
|
+
return `No results found for "${args.query}"${searchLocation}.`;
|
|
1253
1215
|
}
|
|
1254
1216
|
const searchResults = posts.map((post, index) => {
|
|
1255
1217
|
const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
|
|
@@ -1272,11 +1234,11 @@ ${searchResults}`;
|
|
|
1272
1234
|
server.addTool({
|
|
1273
1235
|
name: "create_post",
|
|
1274
1236
|
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:
|
|
1276
|
-
subreddit:
|
|
1277
|
-
title:
|
|
1278
|
-
content:
|
|
1279
|
-
is_self:
|
|
1237
|
+
parameters: z.object({
|
|
1238
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1239
|
+
title: z.string().describe("The post title"),
|
|
1240
|
+
content: z.string().describe("The post content (text for self posts, URL for link posts)"),
|
|
1241
|
+
is_self: z.boolean().default(true).describe("Whether this is a self post (text) or link post")
|
|
1280
1242
|
}),
|
|
1281
1243
|
execute: async (args) => {
|
|
1282
1244
|
const client = getRedditClient();
|
|
@@ -1297,9 +1259,9 @@ Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
|
|
|
1297
1259
|
server.addTool({
|
|
1298
1260
|
name: "reply_to_post",
|
|
1299
1261
|
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:
|
|
1301
|
-
post_id:
|
|
1302
|
-
content:
|
|
1262
|
+
parameters: z.object({
|
|
1263
|
+
post_id: z.string().describe("The Reddit post ID (thing_id, e.g., t3_xxxxx for posts, t1_xxxxx for comments)"),
|
|
1264
|
+
content: z.string().describe("The reply content")
|
|
1303
1265
|
}),
|
|
1304
1266
|
execute: async (args) => {
|
|
1305
1267
|
const client = getRedditClient();
|
|
@@ -1319,7 +1281,7 @@ Your reply has been successfully posted.`;
|
|
|
1319
1281
|
server.addTool({
|
|
1320
1282
|
name: "delete_post",
|
|
1321
1283
|
description: "Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
|
|
1322
|
-
parameters:
|
|
1284
|
+
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.") }),
|
|
1323
1285
|
execute: async (args) => {
|
|
1324
1286
|
const client = getRedditClient();
|
|
1325
1287
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1335,7 +1297,7 @@ The post ${args.thing_id} has been permanently deleted from Reddit.
|
|
|
1335
1297
|
server.addTool({
|
|
1336
1298
|
name: "delete_comment",
|
|
1337
1299
|
description: "Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
|
|
1338
|
-
parameters:
|
|
1300
|
+
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.") }),
|
|
1339
1301
|
execute: async (args) => {
|
|
1340
1302
|
const client = getRedditClient();
|
|
1341
1303
|
if (!client) throw new Error("Reddit client not initialized");
|
|
@@ -1351,9 +1313,9 @@ The comment ${args.thing_id} has been permanently deleted from Reddit.
|
|
|
1351
1313
|
server.addTool({
|
|
1352
1314
|
name: "edit_post",
|
|
1353
1315
|
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:
|
|
1355
|
-
thing_id:
|
|
1356
|
-
new_text:
|
|
1316
|
+
parameters: z.object({
|
|
1317
|
+
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."),
|
|
1318
|
+
new_text: z.string().describe("The new text content for the post. Supports Reddit markdown formatting.")
|
|
1357
1319
|
}),
|
|
1358
1320
|
execute: async (args) => {
|
|
1359
1321
|
const client = getRedditClient();
|
|
@@ -1374,9 +1336,9 @@ The post ${args.thing_id} has been updated with your new content.
|
|
|
1374
1336
|
server.addTool({
|
|
1375
1337
|
name: "edit_comment",
|
|
1376
1338
|
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:
|
|
1378
|
-
thing_id:
|
|
1379
|
-
new_text:
|
|
1339
|
+
parameters: z.object({
|
|
1340
|
+
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."),
|
|
1341
|
+
new_text: z.string().describe("The new text content for the comment. Supports Reddit markdown formatting.")
|
|
1380
1342
|
}),
|
|
1381
1343
|
execute: async (args) => {
|
|
1382
1344
|
const client = getRedditClient();
|
|
@@ -1393,10 +1355,10 @@ The comment ${args.thing_id} has been updated with your new content.
|
|
|
1393
1355
|
server.addTool({
|
|
1394
1356
|
name: "get_post_comments",
|
|
1395
1357
|
description: "Get comments from a specific Reddit post",
|
|
1396
|
-
parameters:
|
|
1397
|
-
post_id:
|
|
1398
|
-
subreddit:
|
|
1399
|
-
sort:
|
|
1358
|
+
parameters: z.object({
|
|
1359
|
+
post_id: z.string().describe("The Reddit post ID"),
|
|
1360
|
+
subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
|
|
1361
|
+
sort: z.enum([
|
|
1400
1362
|
"best",
|
|
1401
1363
|
"top",
|
|
1402
1364
|
"new",
|
|
@@ -1404,7 +1366,7 @@ server.addTool({
|
|
|
1404
1366
|
"old",
|
|
1405
1367
|
"qa"
|
|
1406
1368
|
]).default("best").describe("Comment sort order"),
|
|
1407
|
-
limit:
|
|
1369
|
+
limit: z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
|
|
1408
1370
|
}),
|
|
1409
1371
|
execute: async (args) => {
|
|
1410
1372
|
const client = getRedditClient();
|
|
@@ -1414,8 +1376,8 @@ server.addTool({
|
|
|
1414
1376
|
sort: args.sort,
|
|
1415
1377
|
limit: args.limit
|
|
1416
1378
|
});
|
|
1417
|
-
const post = data
|
|
1418
|
-
const comments = data
|
|
1379
|
+
const { post } = data;
|
|
1380
|
+
const { comments } = data;
|
|
1419
1381
|
let response = `# Comments for: ${post.title}
|
|
1420
1382
|
|
|
1421
1383
|
**Post by u/${post.author} in r/${post.subreddit}**
|
|
@@ -1448,7 +1410,7 @@ async function main() {
|
|
|
1448
1410
|
await setupRedditClient();
|
|
1449
1411
|
const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http";
|
|
1450
1412
|
const port = parseInt(process.env.PORT || "3000");
|
|
1451
|
-
const host = process.env.HOST || "
|
|
1413
|
+
const host = process.env.HOST || "127.0.0.1";
|
|
1452
1414
|
if (useHttp) {
|
|
1453
1415
|
console.error(`[Setup] Starting HTTP server on ${host}:${port}`);
|
|
1454
1416
|
await server.start({
|
|
@@ -1481,4 +1443,5 @@ process.on("SIGTERM", async () => {
|
|
|
1481
1443
|
main().catch(console.error);
|
|
1482
1444
|
|
|
1483
1445
|
//#endregion
|
|
1446
|
+
export { };
|
|
1484
1447
|
//# sourceMappingURL=index.js.map
|