reddit-mcp-server 1.5.2 → 1.6.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 +62 -46
- package/dist/bin.js +2 -2
- package/dist/index.js +157 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import dotenv from "dotenv";
|
|
|
3
3
|
import { FastMCP } from "fastmcp";
|
|
4
4
|
import { Left, Option, Right, Try } from "functype";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { XMLParser } from "fast-xml-parser";
|
|
6
7
|
//#region src/client/errors.ts
|
|
7
8
|
/**
|
|
8
9
|
* Typed error channel for the Reddit client.
|
|
@@ -59,21 +60,6 @@ var NotFoundError = class extends RedditErrorBase {
|
|
|
59
60
|
this.name = "NotFoundError";
|
|
60
61
|
}
|
|
61
62
|
};
|
|
62
|
-
/**
|
|
63
|
-
* Reddit refused the request at the network level rather than for this specific resource.
|
|
64
|
-
*
|
|
65
|
-
* Reddit 403s the unauthenticated JSON API from many IP ranges (datacenters, VPNs, flagged
|
|
66
|
-
* addresses), answering with an HTML block page instead of JSON. A bare `HttpError(403)` reads
|
|
67
|
-
* as "this subreddit is private", which sends people looking in the wrong place — the fix is to
|
|
68
|
-
* supply OAuth credentials, which also raises the rate limit from ~10 to 60+ req/min.
|
|
69
|
-
*/
|
|
70
|
-
var NetworkBlockedError = class extends RedditErrorBase {
|
|
71
|
-
_tag = "NetworkBlockedError";
|
|
72
|
-
constructor(message) {
|
|
73
|
-
super(message);
|
|
74
|
-
this.name = "NetworkBlockedError";
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
63
|
/** Client-side input or safety-policy rejection (invalid sort, duplicate-content guard). */
|
|
78
64
|
var ValidationError = class extends RedditErrorBase {
|
|
79
65
|
_tag = "ValidationError";
|
|
@@ -231,6 +217,100 @@ var ResponseCache = class {
|
|
|
231
217
|
}
|
|
232
218
|
};
|
|
233
219
|
//#endregion
|
|
220
|
+
//#region src/client/rss-client.ts
|
|
221
|
+
function contentText(content) {
|
|
222
|
+
if (content === void 0) return "";
|
|
223
|
+
if (typeof content === "string") return content;
|
|
224
|
+
return content["#text"] ?? "";
|
|
225
|
+
}
|
|
226
|
+
const parser = new XMLParser({
|
|
227
|
+
ignoreAttributes: false,
|
|
228
|
+
removeNSPrefix: true,
|
|
229
|
+
parseTagValue: false
|
|
230
|
+
});
|
|
231
|
+
function parseAtomFeed(xml) {
|
|
232
|
+
var _result$feed;
|
|
233
|
+
const entries = (_result$feed = parser.parse(xml).feed) === null || _result$feed === void 0 ? void 0 : _result$feed.entry;
|
|
234
|
+
if (!entries) return [];
|
|
235
|
+
return Array.isArray(entries) ? entries : [entries];
|
|
236
|
+
}
|
|
237
|
+
function decodeHtmlEntities(text) {
|
|
238
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/ /g, " ").replace(/&/g, "&");
|
|
239
|
+
}
|
|
240
|
+
function extractLinkUrl(contentHtml) {
|
|
241
|
+
const match = contentHtml.match(/href="([^"]+)">\[link\]/);
|
|
242
|
+
return (match === null || match === void 0 ? void 0 : match[1]) ? decodeHtmlEntities(match[1]) : void 0;
|
|
243
|
+
}
|
|
244
|
+
function extractSelfText(contentHtml) {
|
|
245
|
+
const match = contentHtml.match(/<!-- SC_OFF -->(.*?)<!-- SC_ON -->/s);
|
|
246
|
+
if (!match) return "";
|
|
247
|
+
return decodeHtmlEntities(match[1].replace(/<[^>]+>/g, "")).trim();
|
|
248
|
+
}
|
|
249
|
+
function atomEntryToRedditPost(entry) {
|
|
250
|
+
var _entry$link, _entry$author, _entry$category;
|
|
251
|
+
const permalink = ((_entry$link = entry.link) === null || _entry$link === void 0 ? void 0 : _entry$link["@_href"]) ?? "";
|
|
252
|
+
const contentHtml = contentText(entry.content);
|
|
253
|
+
const externalUrl = extractLinkUrl(contentHtml);
|
|
254
|
+
const isSelf = !externalUrl || externalUrl.includes("/comments/");
|
|
255
|
+
const author = (((_entry$author = entry.author) === null || _entry$author === void 0 ? void 0 : _entry$author.name) ?? "").replace(/^\/u\//, "");
|
|
256
|
+
const subreddit = ((_entry$category = entry.category) === null || _entry$category === void 0 ? void 0 : _entry$category["@_term"]) ?? "";
|
|
257
|
+
return {
|
|
258
|
+
id: (entry.id ?? "").replace(/^t3_/, ""),
|
|
259
|
+
title: entry.title ?? "",
|
|
260
|
+
author,
|
|
261
|
+
subreddit,
|
|
262
|
+
selftext: extractSelfText(contentHtml),
|
|
263
|
+
url: isSelf ? permalink : externalUrl ?? permalink,
|
|
264
|
+
score: 0,
|
|
265
|
+
upvoteRatio: 0,
|
|
266
|
+
numComments: 0,
|
|
267
|
+
createdUtc: Math.floor(new Date(entry.published ?? entry.updated ?? "").getTime() / 1e3),
|
|
268
|
+
over18: false,
|
|
269
|
+
edited: false,
|
|
270
|
+
isSelf,
|
|
271
|
+
permalink: permalink.replace("https://www.reddit.com", "")
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const RSS_CACHE_TTL_MS = 6e4;
|
|
275
|
+
var RssClient = class {
|
|
276
|
+
userAgent;
|
|
277
|
+
cache = /* @__PURE__ */ new Map();
|
|
278
|
+
constructor(userAgent) {
|
|
279
|
+
this.userAgent = userAgent;
|
|
280
|
+
}
|
|
281
|
+
async fetchSubredditPosts(subreddit, sort, timeFilter, limit, after) {
|
|
282
|
+
return (await Try.async(async () => {
|
|
283
|
+
const sub = normalizeSubreddit(subreddit);
|
|
284
|
+
const basePath = sub === "" ? "" : `/r/${sub}`;
|
|
285
|
+
const sortPath = sort === "hot" ? "" : `/${sort}`;
|
|
286
|
+
const params = new URLSearchParams();
|
|
287
|
+
if (timeFilter && (sort === "top" || sort === "controversial")) params.set("t", timeFilter);
|
|
288
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
289
|
+
if (after !== void 0) params.set("after", after);
|
|
290
|
+
const url = `https://www.reddit.com${basePath}${sortPath}/.rss${params.size > 0 ? `?${params}` : ""}`;
|
|
291
|
+
const cached = this.cache.get(url);
|
|
292
|
+
if (cached && Date.now() < cached.expiresAt) return cached.page;
|
|
293
|
+
const response = await fetch(url, { headers: { "User-Agent": this.userAgent } });
|
|
294
|
+
if (response.status === 429) throw new HttpError(429, "RSS rate limit exceeded (~1 req/min for unauthenticated feeds). Try again shortly.");
|
|
295
|
+
if (!response.ok) throw new HttpError(response.status, `RSS feed request failed: ${response.status} ${response.statusText}`);
|
|
296
|
+
const allItems = parseAtomFeed(await response.text()).map(atomEntryToRedditPost);
|
|
297
|
+
const cap = limit ?? 25;
|
|
298
|
+
const items = allItems.length > cap ? allItems.slice(0, cap) : allItems;
|
|
299
|
+
const afterCursor = allItems.length > items.length ? `t3_${items[items.length - 1].id}` : void 0;
|
|
300
|
+
const page = {
|
|
301
|
+
items,
|
|
302
|
+
source: "rss",
|
|
303
|
+
...afterCursor ? { after: afterCursor } : {}
|
|
304
|
+
};
|
|
305
|
+
this.cache.set(url, {
|
|
306
|
+
page,
|
|
307
|
+
expiresAt: Date.now() + RSS_CACHE_TTL_MS
|
|
308
|
+
});
|
|
309
|
+
return page;
|
|
310
|
+
})).toEither((error) => classifyRedditError(error, "RSS fetch"));
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
234
314
|
//#region src/client/reddit-client.ts
|
|
235
315
|
function listingCursor(data) {
|
|
236
316
|
const after = typeof data.after === "string" ? { after: data.after } : {};
|
|
@@ -240,10 +320,6 @@ function listingCursor(data) {
|
|
|
240
320
|
...before
|
|
241
321
|
};
|
|
242
322
|
}
|
|
243
|
-
function headerValue(response, name) {
|
|
244
|
-
const headers = response.headers;
|
|
245
|
-
return (headers === null || headers === void 0 ? void 0 : headers.get(name)) ?? "";
|
|
246
|
-
}
|
|
247
323
|
function parsePostData(post) {
|
|
248
324
|
return {
|
|
249
325
|
id: post.id,
|
|
@@ -277,6 +353,8 @@ var RedditClient = class {
|
|
|
277
353
|
botDisclosure;
|
|
278
354
|
cache;
|
|
279
355
|
retry;
|
|
356
|
+
rssClient;
|
|
357
|
+
usesRss;
|
|
280
358
|
accessToken;
|
|
281
359
|
tokenExpiry = 0;
|
|
282
360
|
authenticated = false;
|
|
@@ -309,6 +387,8 @@ var RedditClient = class {
|
|
|
309
387
|
baseDelayMs: 1e3,
|
|
310
388
|
maxDelayMs: 6e4
|
|
311
389
|
};
|
|
390
|
+
this.usesRss = this.authMode === "anonymous" || this.authMode === "auto" && !this.hasCredentials;
|
|
391
|
+
this.rssClient = new RssClient(this.userAgent);
|
|
312
392
|
}
|
|
313
393
|
determineBaseUrl() {
|
|
314
394
|
switch (this.authMode) {
|
|
@@ -338,7 +418,6 @@ var RedditClient = class {
|
|
|
338
418
|
...headers,
|
|
339
419
|
Authorization: await this.reauthorize()
|
|
340
420
|
}, path, 0) : first;
|
|
341
|
-
if (!requiresAuth && response.status === 403 && !headerValue(response, "content-type").includes("json")) throw new NetworkBlockedError("Reddit is blocking unauthenticated requests from this network (HTTP 403 with a block page). Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET to authenticate with OAuth, which also raises the rate limit from ~10 to 60+ requests/min. See https://www.reddit.com/prefs/apps to create an app.");
|
|
342
421
|
if (cacheable && response.ok) {
|
|
343
422
|
const text = await response.text();
|
|
344
423
|
this.cache.set(url, text, response.status);
|
|
@@ -394,10 +473,8 @@ var RedditClient = class {
|
|
|
394
473
|
return true;
|
|
395
474
|
}
|
|
396
475
|
validateWriteAccess() {
|
|
397
|
-
if (this.
|
|
398
|
-
|
|
399
|
-
throw new NotAuthenticatedError("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
|
|
400
|
-
}
|
|
476
|
+
if (this.usesRss) throw new NotAuthenticatedError("Write operations require OAuth credentials (REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET) in addition to REDDIT_USERNAME and REDDIT_PASSWORD.");
|
|
477
|
+
if (this.username === void 0 || this.password === void 0) throw new NotAuthenticatedError("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
|
|
401
478
|
}
|
|
402
479
|
async enforceWriteRateLimit() {
|
|
403
480
|
if (!this.safeMode.enabled || this.safeMode.writeDelayMs <= 0) return;
|
|
@@ -463,7 +540,11 @@ var RedditClient = class {
|
|
|
463
540
|
if (!this.botDisclosure.enabled || this.botDisclosure.footer === "") return content;
|
|
464
541
|
return `${content}${this.botDisclosure.footer}`;
|
|
465
542
|
}
|
|
543
|
+
requiresOAuthError(tool) {
|
|
544
|
+
return Left(new NotAuthenticatedError(`${tool} requires OAuth credentials (REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET). RSS fallback only supports browse_subreddit and get_top_posts.`));
|
|
545
|
+
}
|
|
466
546
|
async getUser(username) {
|
|
547
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_info");
|
|
467
548
|
const context = `Failed to get user info for ${username}`;
|
|
468
549
|
return (await Try.async(async () => {
|
|
469
550
|
const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/about.json`)).orThrow();
|
|
@@ -512,6 +593,7 @@ var RedditClient = class {
|
|
|
512
593
|
})).toEither((error) => classifyRedditError(error, context));
|
|
513
594
|
}
|
|
514
595
|
async getMyOverview(options = {}) {
|
|
596
|
+
if (this.usesRss) return this.requiresOAuthError("get_my_overview");
|
|
515
597
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your overview requires REDDIT_USERNAME"));
|
|
516
598
|
const { limit = 25, after } = options;
|
|
517
599
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
@@ -519,6 +601,7 @@ var RedditClient = class {
|
|
|
519
601
|
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/overview.json?${params}`, "Failed to get your overview");
|
|
520
602
|
}
|
|
521
603
|
async getMySaved(options = {}) {
|
|
604
|
+
if (this.usesRss) return this.requiresOAuthError("get_my_saved");
|
|
522
605
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching saved content requires REDDIT_USERNAME"));
|
|
523
606
|
const { limit = 25, after } = options;
|
|
524
607
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
@@ -526,6 +609,7 @@ var RedditClient = class {
|
|
|
526
609
|
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/saved.json?${params}`, "Failed to get saved content");
|
|
527
610
|
}
|
|
528
611
|
async getMe() {
|
|
612
|
+
if (this.usesRss) return this.requiresOAuthError("get_me");
|
|
529
613
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your account requires REDDIT_USERNAME"));
|
|
530
614
|
const context = "Failed to get authenticated user info";
|
|
531
615
|
return (await Try.async(async () => {
|
|
@@ -547,6 +631,7 @@ var RedditClient = class {
|
|
|
547
631
|
})).toEither((error) => classifyRedditError(error, context));
|
|
548
632
|
}
|
|
549
633
|
async getSubredditInfo(subredditName) {
|
|
634
|
+
if (this.usesRss) return this.requiresOAuthError("get_subreddit_info");
|
|
550
635
|
const context = `Failed to get subreddit info for ${subredditName}`;
|
|
551
636
|
return (await Try.async(async () => {
|
|
552
637
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subredditName)}/about.json`)).orThrow();
|
|
@@ -567,6 +652,7 @@ var RedditClient = class {
|
|
|
567
652
|
})).toEither((error) => classifyRedditError(error, context));
|
|
568
653
|
}
|
|
569
654
|
async getSubredditRules(subreddit) {
|
|
655
|
+
if (this.usesRss) return this.requiresOAuthError("get_subreddit_rules");
|
|
570
656
|
const context = `Failed to get rules for r/${subreddit}`;
|
|
571
657
|
return (await Try.async(async () => {
|
|
572
658
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/about/rules.json`)).orThrow();
|
|
@@ -582,6 +668,7 @@ var RedditClient = class {
|
|
|
582
668
|
})).toEither((error) => classifyRedditError(error, context));
|
|
583
669
|
}
|
|
584
670
|
async getPostFlairs(subreddit) {
|
|
671
|
+
if (this.usesRss) return this.requiresOAuthError("get_post_flairs");
|
|
585
672
|
const context = `Failed to get post flairs for r/${subreddit}`;
|
|
586
673
|
return (await Try.async(async () => {
|
|
587
674
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/api/link_flair_v2.json`)).orThrow();
|
|
@@ -595,6 +682,7 @@ var RedditClient = class {
|
|
|
595
682
|
})).toEither((error) => classifyRedditError(error, context));
|
|
596
683
|
}
|
|
597
684
|
async getTopPosts(subreddit, timeFilter = "week", limit = 10, after) {
|
|
685
|
+
if (this.usesRss) return this.rssClient.fetchSubredditPosts(subreddit, "top", timeFilter, limit, after);
|
|
598
686
|
const params = new URLSearchParams({
|
|
599
687
|
t: timeFilter,
|
|
600
688
|
limit: limit.toString()
|
|
@@ -622,6 +710,7 @@ var RedditClient = class {
|
|
|
622
710
|
"controversial"
|
|
623
711
|
];
|
|
624
712
|
if (!validSorts.includes(sort)) return Left(new ValidationError(`Invalid sort "${sort}". Valid options are: ${validSorts.join(", ")}`));
|
|
713
|
+
if (this.usesRss) return this.rssClient.fetchSubredditPosts(subreddit, sort, timeFilter, limit, after);
|
|
625
714
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
626
715
|
if (sort === "top" || sort === "controversial") params.set("t", timeFilter);
|
|
627
716
|
if (after !== void 0) params.set("after", after);
|
|
@@ -640,6 +729,7 @@ var RedditClient = class {
|
|
|
640
729
|
})).toEither((error) => classifyRedditError(error, context));
|
|
641
730
|
}
|
|
642
731
|
async getPost(postId, subreddit) {
|
|
732
|
+
if (this.usesRss) return this.requiresOAuthError("get_reddit_post");
|
|
643
733
|
const context = `Failed to get post with ID ${postId}`;
|
|
644
734
|
return (await Try.async(async () => {
|
|
645
735
|
const id = normalizeThingId(postId);
|
|
@@ -653,6 +743,7 @@ var RedditClient = class {
|
|
|
653
743
|
})).toEither((error) => classifyRedditError(error, context));
|
|
654
744
|
}
|
|
655
745
|
async getTrendingSubreddits(limit = 5) {
|
|
746
|
+
if (this.usesRss) return this.requiresOAuthError("get_trending_subreddits");
|
|
656
747
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
657
748
|
const context = `Failed to get trending subreddits`;
|
|
658
749
|
return (await Try.async(async () => {
|
|
@@ -800,6 +891,7 @@ var RedditClient = class {
|
|
|
800
891
|
return this.editThing(thingId, newText, "t1");
|
|
801
892
|
}
|
|
802
893
|
async searchReddit(query, options = {}) {
|
|
894
|
+
if (this.usesRss) return this.requiresOAuthError("search_reddit");
|
|
803
895
|
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link", after, before } = options;
|
|
804
896
|
const params = new URLSearchParams({
|
|
805
897
|
q: query,
|
|
@@ -824,6 +916,7 @@ var RedditClient = class {
|
|
|
824
916
|
})).toEither((error) => classifyRedditError(error, context));
|
|
825
917
|
}
|
|
826
918
|
async getPostComments(postId, subreddit, options = {}) {
|
|
919
|
+
if (this.usesRss) return this.requiresOAuthError("get_post_comments");
|
|
827
920
|
const { sort = "best", limit = 100 } = options;
|
|
828
921
|
const params = new URLSearchParams({
|
|
829
922
|
sort,
|
|
@@ -863,6 +956,7 @@ var RedditClient = class {
|
|
|
863
956
|
})).toEither((error) => classifyRedditError(error, context));
|
|
864
957
|
}
|
|
865
958
|
async getMoreComments(linkId, commentIds) {
|
|
959
|
+
if (this.usesRss) return this.requiresOAuthError("get_more_comments");
|
|
866
960
|
const context = `Failed to expand comments for ${linkId}`;
|
|
867
961
|
return (await Try.async(async () => {
|
|
868
962
|
var _json$json$data4;
|
|
@@ -893,6 +987,7 @@ var RedditClient = class {
|
|
|
893
987
|
})).toEither((error) => classifyRedditError(error, context));
|
|
894
988
|
}
|
|
895
989
|
async getUserPosts(username, options = {}) {
|
|
990
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_posts");
|
|
896
991
|
const { sort = "new", timeFilter = "all", limit = 25, after } = options;
|
|
897
992
|
const params = new URLSearchParams({
|
|
898
993
|
sort,
|
|
@@ -912,6 +1007,7 @@ var RedditClient = class {
|
|
|
912
1007
|
})).toEither((error) => classifyRedditError(error, context));
|
|
913
1008
|
}
|
|
914
1009
|
async getUserComments(username, options = {}) {
|
|
1010
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_comments");
|
|
915
1011
|
const { sort = "new", timeFilter = "all", limit = 25, after } = options;
|
|
916
1012
|
const params = new URLSearchParams({
|
|
917
1013
|
sort,
|
|
@@ -1085,7 +1181,7 @@ function formatSubredditInfo(subreddit) {
|
|
|
1085
1181
|
//#endregion
|
|
1086
1182
|
//#region src/index.ts
|
|
1087
1183
|
dotenv.config({ quiet: true });
|
|
1088
|
-
const VERSION = "1.
|
|
1184
|
+
const VERSION = "1.6.0";
|
|
1089
1185
|
function validateUserAgent(userAgent, username) {
|
|
1090
1186
|
if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
|
|
1091
1187
|
console.error("[Warning] User-Agent does not follow Reddit's recommended format");
|
|
@@ -1136,6 +1232,19 @@ function buildSafeModeConfig(safeMode) {
|
|
|
1136
1232
|
function unwrapClient() {
|
|
1137
1233
|
return getRedditClient().orThrow(/* @__PURE__ */ new Error("Reddit client not initialized"));
|
|
1138
1234
|
}
|
|
1235
|
+
function rssDisclaimer(source) {
|
|
1236
|
+
return source === "rss" ? "\n\n> **RSS mode** — scores, comment counts, and upvote ratios are unavailable. Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET for full data." : "";
|
|
1237
|
+
}
|
|
1238
|
+
function formatPostSummary(post, index, source) {
|
|
1239
|
+
const statsLines = source === "rss" ? [] : [`- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)`, `- Comments: ${post.stats.comments.toLocaleString()}`];
|
|
1240
|
+
return [
|
|
1241
|
+
`### ${index + 1}. ${post.title}`,
|
|
1242
|
+
`- Author: u/${post.author}`,
|
|
1243
|
+
...statsLines,
|
|
1244
|
+
`- Posted: ${post.metadata.posted}`,
|
|
1245
|
+
`- Link: ${post.links.shortLink}`
|
|
1246
|
+
].join("\n");
|
|
1247
|
+
}
|
|
1139
1248
|
function nextPageHint(after) {
|
|
1140
1249
|
return Option(after).fold(() => "", (cursor) => `\n\n---\nMore results available — call again with after="${cursor}" for the next page.`);
|
|
1141
1250
|
}
|
|
@@ -1208,9 +1317,12 @@ async function setupRedditClient() {
|
|
|
1208
1317
|
});
|
|
1209
1318
|
console.error("[Setup] Reddit client initialized");
|
|
1210
1319
|
console.error(`[Setup] Authentication mode: ${authMode}`);
|
|
1320
|
+
if (authMode === "anonymous") console.error("[Warning] REDDIT_AUTH_MODE=anonymous is deprecated; it is now an alias for auto without credentials.");
|
|
1211
1321
|
if (authMode === "anonymous" || !hasCredentials) {
|
|
1212
|
-
console.error("[Setup]
|
|
1213
|
-
console.error("[Setup]
|
|
1322
|
+
console.error("[Setup] RSS fallback mode — no OAuth credentials detected.");
|
|
1323
|
+
console.error("[Setup] Only browse_subreddit and get_top_posts are available (~1 req/min, no engagement metrics).");
|
|
1324
|
+
console.error("[Setup] Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET for full API access (100 req/min).");
|
|
1325
|
+
console.error("[Setup] See: https://www.reddit.com/wiki/api");
|
|
1214
1326
|
} else {
|
|
1215
1327
|
console.error("[Setup] Testing Reddit API connection...");
|
|
1216
1328
|
if (!await client.checkAuthentication()) {
|
|
@@ -1308,7 +1420,7 @@ Ready to handle Reddit API requests!`);
|
|
|
1308
1420
|
});
|
|
1309
1421
|
server.addTool({
|
|
1310
1422
|
name: "get_user_info",
|
|
1311
|
-
description: "Get a public profile for any Reddit user: comment/post/total karma, account age and status flags, plus a short activity analysis and engagement tips. Read-only;
|
|
1423
|
+
description: "Get a public profile for any Reddit user: comment/post/total karma, account age and status flags, plus a short activity analysis and engagement tips. Read-only; requires OAuth credentials. Returns profile stats only — use get_user_posts / get_user_comments for their actual content. Use get_me instead for your own authenticated account; do NOT expect private fields here, as only public data is returned.",
|
|
1312
1424
|
annotations: {
|
|
1313
1425
|
title: "Get User Info",
|
|
1314
1426
|
readOnlyHint: true,
|
|
@@ -1342,7 +1454,7 @@ server.addTool({
|
|
|
1342
1454
|
});
|
|
1343
1455
|
server.addTool({
|
|
1344
1456
|
name: "get_me",
|
|
1345
|
-
description: "Get the authenticated user's own profile (karma, account age, status flags). Read-only, but requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD) and fails
|
|
1457
|
+
description: "Get the authenticated user's own profile (karma, account age, status flags). Read-only, but requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD) and fails without user credentials. Use this instead of get_user_info when you need the current account rather than an arbitrary user. Do NOT use it to look up other users — it always returns the logged-in account.",
|
|
1346
1458
|
annotations: {
|
|
1347
1459
|
title: "Get My Account",
|
|
1348
1460
|
readOnlyHint: true,
|
|
@@ -1412,7 +1524,7 @@ server.addTool({
|
|
|
1412
1524
|
});
|
|
1413
1525
|
server.addTool({
|
|
1414
1526
|
name: "get_user_posts",
|
|
1415
|
-
description: "Get posts submitted by a specific user, with sort (new/hot/top) and time filter. Read-only;
|
|
1527
|
+
description: "Get posts submitted by a specific user, with sort (new/hot/top) and time filter. Read-only; requires OAuth credentials. Returns a page of posts (title, subreddit, score, upvote ratio, comment count, permalink) plus an `after` cursor for paging. Use get_user_comments for their comments, or get_user_info for karma/profile stats. Do NOT use this to search a subreddit — use search_reddit or browse_subreddit.",
|
|
1416
1528
|
annotations: {
|
|
1417
1529
|
title: "Get User Posts",
|
|
1418
1530
|
readOnlyHint: true,
|
|
@@ -1464,7 +1576,7 @@ ${postSummaries}${nextPageHint(page.after)}`;
|
|
|
1464
1576
|
});
|
|
1465
1577
|
server.addTool({
|
|
1466
1578
|
name: "get_user_comments",
|
|
1467
|
-
description: "Get comments made by a specific user, with sort (new/hot/top) and time filter. Read-only;
|
|
1579
|
+
description: "Get comments made by a specific user, with sort (new/hot/top) and time filter. Read-only; requires OAuth credentials. Returns a page of comments (subreddit, parent post title, body excerpt, score, permalink) plus an `after` cursor. Use get_user_posts for their submissions, or get_user_info for karma/profile stats. Do NOT use this to read one post's thread — use get_post_comments.",
|
|
1468
1580
|
annotations: {
|
|
1469
1581
|
title: "Get User Comments",
|
|
1470
1582
|
readOnlyHint: true,
|
|
@@ -1519,7 +1631,7 @@ ${commentSummaries}${nextPageHint(page.after)}`;
|
|
|
1519
1631
|
});
|
|
1520
1632
|
server.addTool({
|
|
1521
1633
|
name: "get_reddit_post",
|
|
1522
|
-
description: "Get a single post by subreddit + post id: title, author, self-text or link content, score, upvote ratio, comment count, flair/flags, and an engagement analysis. Read-only;
|
|
1634
|
+
description: "Get a single post by subreddit + post id: title, author, self-text or link content, score, upvote ratio, comment count, flair/flags, and an engagement analysis. Read-only; requires OAuth credentials. Returns the post only — use get_post_comments for its comment thread. Do NOT use this to list a subreddit's posts (use browse_subreddit / get_top_posts) or to find posts by keyword (use search_reddit).",
|
|
1523
1635
|
annotations: {
|
|
1524
1636
|
title: "Get Reddit Post",
|
|
1525
1637
|
readOnlyHint: true,
|
|
@@ -1568,7 +1680,7 @@ ${formattedPost.bestTimeToEngage}`;
|
|
|
1568
1680
|
});
|
|
1569
1681
|
server.addTool({
|
|
1570
1682
|
name: "get_top_posts",
|
|
1571
|
-
description: "Get the top-scoring posts from a subreddit — or from the authenticated home feed if no subreddit is given — within a time window (hour…all). Read-only; works
|
|
1683
|
+
description: "Get the top-scoring posts from a subreddit — or from the authenticated home feed if no subreddit is given — within a time window (hour…all). Read-only; works without credentials via RSS fallback (titles and links only, no scores or comment counts). Returns a page of posts plus an `after` cursor. This is a shortcut for the 'top' sort; use browse_subreddit for hot/new/rising/controversial, or search_reddit to find posts by keyword.",
|
|
1572
1684
|
annotations: {
|
|
1573
1685
|
title: "Get Top Posts",
|
|
1574
1686
|
readOnlyHint: true,
|
|
@@ -1593,21 +1705,16 @@ server.addTool({
|
|
|
1593
1705
|
}, (page) => {
|
|
1594
1706
|
const posts = page.items;
|
|
1595
1707
|
if (posts.length === 0) return `No posts found in ${Option(args.subreddit).fold(() => "home feed", (sr) => `r/${sr}`)} for the specified time period.`;
|
|
1596
|
-
const postSummaries = posts.map(formatPostInfo).map((post, index) =>
|
|
1597
|
-
- Author: u/${post.author}
|
|
1598
|
-
- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
1599
|
-
- Comments: ${post.stats.comments.toLocaleString()}
|
|
1600
|
-
- Posted: ${post.metadata.posted}
|
|
1601
|
-
- Link: ${post.links.shortLink}`).join("\n\n");
|
|
1708
|
+
const postSummaries = posts.map(formatPostInfo).map((post, index) => formatPostSummary(post, index, page.source)).join("\n\n");
|
|
1602
1709
|
return `# Top Posts from ${Option(args.subreddit).fold(() => "Home Feed", (sr) => `r/${sr}`)} (${args.time_filter})
|
|
1603
1710
|
|
|
1604
|
-
${postSummaries}${nextPageHint(page.after)}`;
|
|
1711
|
+
${postSummaries}${nextPageHint(page.after)}${rssDisclaimer(page.source)}`;
|
|
1605
1712
|
});
|
|
1606
1713
|
}
|
|
1607
1714
|
});
|
|
1608
1715
|
server.addTool({
|
|
1609
1716
|
name: "browse_subreddit",
|
|
1610
|
-
description: "Browse a subreddit — or the authenticated home feed when no subreddit is given — by sort order: hot, new, top, rising, or controversial. Read-only; works
|
|
1717
|
+
description: "Browse a subreddit — or the authenticated home feed when no subreddit is given — by sort order: hot, new, top, rising, or controversial. Read-only; works without credentials via RSS fallback (titles and links only, no scores or comment counts). `time_filter` applies only to the top and controversial sorts. Returns a page of posts plus an `after` cursor. Use get_top_posts as a shortcut for the top sort, or search_reddit to find posts by keyword rather than by feed order.",
|
|
1611
1718
|
annotations: {
|
|
1612
1719
|
title: "Browse Subreddit",
|
|
1613
1720
|
readOnlyHint: true,
|
|
@@ -1640,23 +1747,18 @@ server.addTool({
|
|
|
1640
1747
|
const posts = page.items;
|
|
1641
1748
|
const location = Option(args.subreddit).fold(() => "home feed", (sr) => `r/${sr}`);
|
|
1642
1749
|
if (posts.length === 0) return `No posts found in ${location}.`;
|
|
1643
|
-
const postSummaries = posts.map(formatPostInfo).map((post, index) =>
|
|
1644
|
-
- Author: u/${post.author}
|
|
1645
|
-
- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
|
|
1646
|
-
- Comments: ${post.stats.comments.toLocaleString()}
|
|
1647
|
-
- Posted: ${post.metadata.posted}
|
|
1648
|
-
- Link: ${post.links.shortLink}`).join("\n\n");
|
|
1750
|
+
const postSummaries = posts.map(formatPostInfo).map((post, index) => formatPostSummary(post, index, page.source)).join("\n\n");
|
|
1649
1751
|
const timeSuffix = args.sort === "top" || args.sort === "controversial" ? `, ${args.time_filter}` : "";
|
|
1650
1752
|
const heading = location === "home feed" ? "Home Feed" : location;
|
|
1651
1753
|
return `# ${args.sort} posts from ${heading} (${args.sort}${timeSuffix})
|
|
1652
1754
|
|
|
1653
|
-
${postSummaries}${nextPageHint(page.after)}`;
|
|
1755
|
+
${postSummaries}${nextPageHint(page.after)}${rssDisclaimer(page.source)}`;
|
|
1654
1756
|
});
|
|
1655
1757
|
}
|
|
1656
1758
|
});
|
|
1657
1759
|
server.addTool({
|
|
1658
1760
|
name: "get_subreddit_info",
|
|
1659
|
-
description: "Get a subreddit's profile: title, description, subscriber and active-user counts, creation date, flags, wiki/link URLs, plus a community analysis and posting tips. Read-only;
|
|
1761
|
+
description: "Get a subreddit's profile: title, description, subscriber and active-user counts, creation date, flags, wiki/link URLs, plus a community analysis and posting tips. Read-only; requires OAuth credentials. Returns metadata about the community itself — use browse_subreddit / get_top_posts for its posts, or get_subreddit_rules for its posting rules. Do NOT use this to find subreddits by topic — use search_reddit with type='sr'.",
|
|
1660
1762
|
annotations: {
|
|
1661
1763
|
title: "Get Subreddit Info",
|
|
1662
1764
|
readOnlyHint: true,
|
|
@@ -1700,7 +1802,7 @@ ${formattedSubreddit.description.full}
|
|
|
1700
1802
|
});
|
|
1701
1803
|
server.addTool({
|
|
1702
1804
|
name: "get_subreddit_rules",
|
|
1703
|
-
description: "Get a subreddit's posting rules (each rule's name, what it applies to, and its description). Read-only;
|
|
1805
|
+
description: "Get a subreddit's posting rules (each rule's name, what it applies to, and its description). Read-only; requires OAuth credentials. Returns the rules list, or a note when the subreddit lists none. Call this before create_post to check requirements and avoid auto-removal. For available post flairs use get_post_flairs instead.",
|
|
1704
1806
|
annotations: {
|
|
1705
1807
|
title: "Get Subreddit Rules",
|
|
1706
1808
|
readOnlyHint: true,
|
|
@@ -1725,7 +1827,7 @@ ${ruleList}`;
|
|
|
1725
1827
|
});
|
|
1726
1828
|
server.addTool({
|
|
1727
1829
|
name: "get_post_flairs",
|
|
1728
|
-
description: "List a subreddit's selectable link flairs (flair text + flair_id) for use with create_post. Read-only, but requires user credentials; many subreddits expose flairs only to members, so this can 403 or return empty
|
|
1830
|
+
description: "List a subreddit's selectable link flairs (flair text + flair_id) for use with create_post. Read-only, but requires user credentials; many subreddits expose flairs only to members, so this can 403 or return empty without credentials. Pass a returned flair_id (and flair_text for text-editable flairs) to create_post. For the subreddit's posting rules use get_subreddit_rules instead.",
|
|
1729
1831
|
annotations: {
|
|
1730
1832
|
title: "Get Post Flairs",
|
|
1731
1833
|
readOnlyHint: true,
|
|
@@ -1751,7 +1853,7 @@ Pass the desired \`flair_id\` to \`create_post\`.`;
|
|
|
1751
1853
|
});
|
|
1752
1854
|
server.addTool({
|
|
1753
1855
|
name: "get_trending_subreddits",
|
|
1754
|
-
description: "Get the subreddits Reddit is currently featuring as trending/popular. Read-only, no parameters;
|
|
1856
|
+
description: "Get the subreddits Reddit is currently featuring as trending/popular. Read-only, no parameters; requires OAuth credentials. Returns a list of subreddit names that changes through the day (cached briefly server-side). To find subreddits by keyword instead of by trend, use search_reddit with type='sr'.",
|
|
1755
1857
|
annotations: {
|
|
1756
1858
|
title: "Get Trending Subreddits",
|
|
1757
1859
|
readOnlyHint: true,
|
|
@@ -1768,7 +1870,7 @@ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).j
|
|
|
1768
1870
|
});
|
|
1769
1871
|
server.addTool({
|
|
1770
1872
|
name: "search_reddit",
|
|
1771
|
-
description: "Search Reddit for posts — or subreddits/users via `type` — optionally scoped to one subreddit, with sort and time filters. Read-only;
|
|
1873
|
+
description: "Search Reddit for posts — or subreddits/users via `type` — optionally scoped to one subreddit, with sort and time filters. Read-only; requires OAuth credentials. Returns a page of results (title, subreddit, author, score, comments, link) plus an `after` cursor for paging. Use this to find content by keyword; use browse_subreddit / get_top_posts to list a known subreddit's feed instead.",
|
|
1772
1874
|
annotations: {
|
|
1773
1875
|
title: "Search Reddit",
|
|
1774
1876
|
readOnlyHint: true,
|
|
@@ -2007,7 +2109,7 @@ The comment ${args.thing_id} has been updated with your new content.
|
|
|
2007
2109
|
});
|
|
2008
2110
|
server.addTool({
|
|
2009
2111
|
name: "get_post_comments",
|
|
2010
|
-
description: "Get the comment thread for a post (by post id + subreddit), sorted best/top/new/controversial/old/qa. Read-only;
|
|
2112
|
+
description: "Get the comment thread for a post (by post id + subreddit), sorted best/top/new/controversial/old/qa. Read-only; requires OAuth credentials. Returns the post header plus threaded comments (author, OP/edited badges, score, body, nesting depth) up to `limit`. Long threads are truncated with 'load more' stubs — expand those with get_more_comments. Use get_reddit_post for just the post body, not the thread.",
|
|
2011
2113
|
annotations: {
|
|
2012
2114
|
title: "Get Post Comments",
|
|
2013
2115
|
readOnlyHint: true,
|
|
@@ -2060,7 +2162,7 @@ ${comment.body}
|
|
|
2060
2162
|
});
|
|
2061
2163
|
server.addTool({
|
|
2062
2164
|
name: "get_more_comments",
|
|
2063
|
-
description: "Expand truncated 'load more comments' stubs in a thread. Read-only;
|
|
2165
|
+
description: "Expand truncated 'load more comments' stubs in a thread. Read-only; requires OAuth credentials. Pass the post's link id and the comment ids from a 'more' node (surfaced by get_post_comments) to fetch those hidden comments; returns the expanded comments (author, body excerpt, score, link). Call get_post_comments first to obtain the thread and its 'more' node ids — do NOT invent ids.",
|
|
2064
2166
|
annotations: {
|
|
2065
2167
|
title: "Get More Comments",
|
|
2066
2168
|
readOnlyHint: true,
|