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