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