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