reddit-mcp-server 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -61
- package/dist/bin.js +2 -2
- package/dist/index.js +267 -56
- 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,
|
|
@@ -264,6 +340,42 @@ function parsePostData(post) {
|
|
|
264
340
|
permalink: post.permalink
|
|
265
341
|
};
|
|
266
342
|
}
|
|
343
|
+
function parseSubredditSearchData(sub) {
|
|
344
|
+
return {
|
|
345
|
+
id: String(sub.id ?? sub.display_name),
|
|
346
|
+
title: `r/${sub.display_name} (${sub.subscribers.toLocaleString("en-US")} subscribers)${sub.title ? ` — ${sub.title}` : ""}`,
|
|
347
|
+
author: "",
|
|
348
|
+
subreddit: sub.display_name,
|
|
349
|
+
selftext: sub.public_description,
|
|
350
|
+
url: `https://reddit.com${sub.url}`,
|
|
351
|
+
score: sub.subscribers,
|
|
352
|
+
upvoteRatio: 1,
|
|
353
|
+
numComments: 0,
|
|
354
|
+
createdUtc: sub.created_utc,
|
|
355
|
+
over18: sub.over18,
|
|
356
|
+
edited: false,
|
|
357
|
+
isSelf: false,
|
|
358
|
+
permalink: sub.url
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function parseUserSearchData(user) {
|
|
362
|
+
const karma = user.total_karma ?? user.link_karma + user.comment_karma;
|
|
363
|
+
return {
|
|
364
|
+
id: user.id,
|
|
365
|
+
title: `u/${user.name} (${karma.toLocaleString("en-US")} karma)`,
|
|
366
|
+
author: user.name,
|
|
367
|
+
subreddit: "",
|
|
368
|
+
url: `https://reddit.com/user/${user.name}`,
|
|
369
|
+
score: karma,
|
|
370
|
+
upvoteRatio: 1,
|
|
371
|
+
numComments: 0,
|
|
372
|
+
createdUtc: user.created_utc,
|
|
373
|
+
over18: false,
|
|
374
|
+
edited: false,
|
|
375
|
+
isSelf: false,
|
|
376
|
+
permalink: `/user/${user.name}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
267
379
|
var RedditClient = class {
|
|
268
380
|
clientId;
|
|
269
381
|
clientSecret;
|
|
@@ -277,6 +389,8 @@ var RedditClient = class {
|
|
|
277
389
|
botDisclosure;
|
|
278
390
|
cache;
|
|
279
391
|
retry;
|
|
392
|
+
rssClient;
|
|
393
|
+
usesRss;
|
|
280
394
|
accessToken;
|
|
281
395
|
tokenExpiry = 0;
|
|
282
396
|
authenticated = false;
|
|
@@ -309,6 +423,8 @@ var RedditClient = class {
|
|
|
309
423
|
baseDelayMs: 1e3,
|
|
310
424
|
maxDelayMs: 6e4
|
|
311
425
|
};
|
|
426
|
+
this.usesRss = this.authMode === "anonymous" || this.authMode === "auto" && !this.hasCredentials;
|
|
427
|
+
this.rssClient = new RssClient(this.userAgent);
|
|
312
428
|
}
|
|
313
429
|
determineBaseUrl() {
|
|
314
430
|
switch (this.authMode) {
|
|
@@ -338,7 +454,6 @@ var RedditClient = class {
|
|
|
338
454
|
...headers,
|
|
339
455
|
Authorization: await this.reauthorize()
|
|
340
456
|
}, 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
457
|
if (cacheable && response.ok) {
|
|
343
458
|
const text = await response.text();
|
|
344
459
|
this.cache.set(url, text, response.status);
|
|
@@ -394,10 +509,8 @@ var RedditClient = class {
|
|
|
394
509
|
return true;
|
|
395
510
|
}
|
|
396
511
|
validateWriteAccess() {
|
|
397
|
-
if (this.
|
|
398
|
-
|
|
399
|
-
throw new NotAuthenticatedError("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
|
|
400
|
-
}
|
|
512
|
+
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.");
|
|
513
|
+
if (this.username === void 0 || this.password === void 0) throw new NotAuthenticatedError("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
|
|
401
514
|
}
|
|
402
515
|
async enforceWriteRateLimit() {
|
|
403
516
|
if (!this.safeMode.enabled || this.safeMode.writeDelayMs <= 0) return;
|
|
@@ -463,7 +576,11 @@ var RedditClient = class {
|
|
|
463
576
|
if (!this.botDisclosure.enabled || this.botDisclosure.footer === "") return content;
|
|
464
577
|
return `${content}${this.botDisclosure.footer}`;
|
|
465
578
|
}
|
|
579
|
+
requiresOAuthError(tool) {
|
|
580
|
+
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.`));
|
|
581
|
+
}
|
|
466
582
|
async getUser(username) {
|
|
583
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_info");
|
|
467
584
|
const context = `Failed to get user info for ${username}`;
|
|
468
585
|
return (await Try.async(async () => {
|
|
469
586
|
const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/about.json`)).orThrow();
|
|
@@ -512,6 +629,7 @@ var RedditClient = class {
|
|
|
512
629
|
})).toEither((error) => classifyRedditError(error, context));
|
|
513
630
|
}
|
|
514
631
|
async getMyOverview(options = {}) {
|
|
632
|
+
if (this.usesRss) return this.requiresOAuthError("get_my_overview");
|
|
515
633
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your overview requires REDDIT_USERNAME"));
|
|
516
634
|
const { limit = 25, after } = options;
|
|
517
635
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
@@ -519,6 +637,7 @@ var RedditClient = class {
|
|
|
519
637
|
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/overview.json?${params}`, "Failed to get your overview");
|
|
520
638
|
}
|
|
521
639
|
async getMySaved(options = {}) {
|
|
640
|
+
if (this.usesRss) return this.requiresOAuthError("get_my_saved");
|
|
522
641
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching saved content requires REDDIT_USERNAME"));
|
|
523
642
|
const { limit = 25, after } = options;
|
|
524
643
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
@@ -526,6 +645,7 @@ var RedditClient = class {
|
|
|
526
645
|
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/saved.json?${params}`, "Failed to get saved content");
|
|
527
646
|
}
|
|
528
647
|
async getMe() {
|
|
648
|
+
if (this.usesRss) return this.requiresOAuthError("get_me");
|
|
529
649
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your account requires REDDIT_USERNAME"));
|
|
530
650
|
const context = "Failed to get authenticated user info";
|
|
531
651
|
return (await Try.async(async () => {
|
|
@@ -547,6 +667,7 @@ var RedditClient = class {
|
|
|
547
667
|
})).toEither((error) => classifyRedditError(error, context));
|
|
548
668
|
}
|
|
549
669
|
async getSubredditInfo(subredditName) {
|
|
670
|
+
if (this.usesRss) return this.requiresOAuthError("get_subreddit_info");
|
|
550
671
|
const context = `Failed to get subreddit info for ${subredditName}`;
|
|
551
672
|
return (await Try.async(async () => {
|
|
552
673
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subredditName)}/about.json`)).orThrow();
|
|
@@ -567,6 +688,7 @@ var RedditClient = class {
|
|
|
567
688
|
})).toEither((error) => classifyRedditError(error, context));
|
|
568
689
|
}
|
|
569
690
|
async getSubredditRules(subreddit) {
|
|
691
|
+
if (this.usesRss) return this.requiresOAuthError("get_subreddit_rules");
|
|
570
692
|
const context = `Failed to get rules for r/${subreddit}`;
|
|
571
693
|
return (await Try.async(async () => {
|
|
572
694
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/about/rules.json`)).orThrow();
|
|
@@ -582,6 +704,7 @@ var RedditClient = class {
|
|
|
582
704
|
})).toEither((error) => classifyRedditError(error, context));
|
|
583
705
|
}
|
|
584
706
|
async getPostFlairs(subreddit) {
|
|
707
|
+
if (this.usesRss) return this.requiresOAuthError("get_post_flairs");
|
|
585
708
|
const context = `Failed to get post flairs for r/${subreddit}`;
|
|
586
709
|
return (await Try.async(async () => {
|
|
587
710
|
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/api/link_flair_v2.json`)).orThrow();
|
|
@@ -595,6 +718,7 @@ var RedditClient = class {
|
|
|
595
718
|
})).toEither((error) => classifyRedditError(error, context));
|
|
596
719
|
}
|
|
597
720
|
async getTopPosts(subreddit, timeFilter = "week", limit = 10, after) {
|
|
721
|
+
if (this.usesRss) return this.rssClient.fetchSubredditPosts(subreddit, "top", timeFilter, limit, after);
|
|
598
722
|
const params = new URLSearchParams({
|
|
599
723
|
t: timeFilter,
|
|
600
724
|
limit: limit.toString()
|
|
@@ -622,6 +746,7 @@ var RedditClient = class {
|
|
|
622
746
|
"controversial"
|
|
623
747
|
];
|
|
624
748
|
if (!validSorts.includes(sort)) return Left(new ValidationError(`Invalid sort "${sort}". Valid options are: ${validSorts.join(", ")}`));
|
|
749
|
+
if (this.usesRss) return this.rssClient.fetchSubredditPosts(subreddit, sort, timeFilter, limit, after);
|
|
625
750
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
626
751
|
if (sort === "top" || sort === "controversial") params.set("t", timeFilter);
|
|
627
752
|
if (after !== void 0) params.set("after", after);
|
|
@@ -640,6 +765,7 @@ var RedditClient = class {
|
|
|
640
765
|
})).toEither((error) => classifyRedditError(error, context));
|
|
641
766
|
}
|
|
642
767
|
async getPost(postId, subreddit) {
|
|
768
|
+
if (this.usesRss) return this.requiresOAuthError("get_reddit_post");
|
|
643
769
|
const context = `Failed to get post with ID ${postId}`;
|
|
644
770
|
return (await Try.async(async () => {
|
|
645
771
|
const id = normalizeThingId(postId);
|
|
@@ -653,6 +779,7 @@ var RedditClient = class {
|
|
|
653
779
|
})).toEither((error) => classifyRedditError(error, context));
|
|
654
780
|
}
|
|
655
781
|
async getTrendingSubreddits(limit = 5) {
|
|
782
|
+
if (this.usesRss) return this.requiresOAuthError("get_trending_subreddits");
|
|
656
783
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
657
784
|
const context = `Failed to get trending subreddits`;
|
|
658
785
|
return (await Try.async(async () => {
|
|
@@ -799,7 +926,30 @@ var RedditClient = class {
|
|
|
799
926
|
async editComment(thingId, newText) {
|
|
800
927
|
return this.editThing(thingId, newText, "t1");
|
|
801
928
|
}
|
|
929
|
+
async setSaved(thingId, saved, category) {
|
|
930
|
+
return (await Try.async(async () => {
|
|
931
|
+
this.validateWriteAccess();
|
|
932
|
+
const fullThingId = normalizeFullname(thingId, "t3");
|
|
933
|
+
const params = new URLSearchParams();
|
|
934
|
+
params.append("id", fullThingId);
|
|
935
|
+
if (saved && category !== void 0) params.append("category", category);
|
|
936
|
+
const response = (await this.makeRequest(saved ? "/api/save" : "/api/unsave", {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
939
|
+
body: params.toString()
|
|
940
|
+
})).orThrow();
|
|
941
|
+
if (!response.ok) throw new HttpError(response.status, `Failed to ${saved ? "save" : "unsave"}: HTTP ${response.status}`);
|
|
942
|
+
return true;
|
|
943
|
+
})).toEither((error) => classifyRedditError(error));
|
|
944
|
+
}
|
|
945
|
+
async saveContent(thingId, category) {
|
|
946
|
+
return this.setSaved(thingId, true, category);
|
|
947
|
+
}
|
|
948
|
+
async unsaveContent(thingId) {
|
|
949
|
+
return this.setSaved(thingId, false);
|
|
950
|
+
}
|
|
802
951
|
async searchReddit(query, options = {}) {
|
|
952
|
+
if (this.usesRss) return this.requiresOAuthError("search_reddit");
|
|
803
953
|
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link", after, before } = options;
|
|
804
954
|
const params = new URLSearchParams({
|
|
805
955
|
q: query,
|
|
@@ -818,12 +968,18 @@ var RedditClient = class {
|
|
|
818
968
|
if (!response.ok) throw new HttpError(response.status, `Failed to search Reddit: HTTP ${response.status}`);
|
|
819
969
|
const json = await response.json();
|
|
820
970
|
return {
|
|
821
|
-
items: json.data.children.
|
|
971
|
+
items: json.data.children.flatMap((child) => {
|
|
972
|
+
if (child.kind === "t3") return [parsePostData(child.data)];
|
|
973
|
+
if (child.kind === "t5") return [parseSubredditSearchData(child.data)];
|
|
974
|
+
if (child.kind === "t2") return [parseUserSearchData(child.data)];
|
|
975
|
+
return [];
|
|
976
|
+
}),
|
|
822
977
|
...listingCursor(json.data)
|
|
823
978
|
};
|
|
824
979
|
})).toEither((error) => classifyRedditError(error, context));
|
|
825
980
|
}
|
|
826
981
|
async getPostComments(postId, subreddit, options = {}) {
|
|
982
|
+
if (this.usesRss) return this.requiresOAuthError("get_post_comments");
|
|
827
983
|
const { sort = "best", limit = 100 } = options;
|
|
828
984
|
const params = new URLSearchParams({
|
|
829
985
|
sort,
|
|
@@ -863,6 +1019,7 @@ var RedditClient = class {
|
|
|
863
1019
|
})).toEither((error) => classifyRedditError(error, context));
|
|
864
1020
|
}
|
|
865
1021
|
async getMoreComments(linkId, commentIds) {
|
|
1022
|
+
if (this.usesRss) return this.requiresOAuthError("get_more_comments");
|
|
866
1023
|
const context = `Failed to expand comments for ${linkId}`;
|
|
867
1024
|
return (await Try.async(async () => {
|
|
868
1025
|
var _json$json$data4;
|
|
@@ -893,6 +1050,7 @@ var RedditClient = class {
|
|
|
893
1050
|
})).toEither((error) => classifyRedditError(error, context));
|
|
894
1051
|
}
|
|
895
1052
|
async getUserPosts(username, options = {}) {
|
|
1053
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_posts");
|
|
896
1054
|
const { sort = "new", timeFilter = "all", limit = 25, after } = options;
|
|
897
1055
|
const params = new URLSearchParams({
|
|
898
1056
|
sort,
|
|
@@ -912,6 +1070,7 @@ var RedditClient = class {
|
|
|
912
1070
|
})).toEither((error) => classifyRedditError(error, context));
|
|
913
1071
|
}
|
|
914
1072
|
async getUserComments(username, options = {}) {
|
|
1073
|
+
if (this.usesRss) return this.requiresOAuthError("get_user_comments");
|
|
915
1074
|
const { sort = "new", timeFilter = "all", limit = 25, after } = options;
|
|
916
1075
|
const params = new URLSearchParams({
|
|
917
1076
|
sort,
|
|
@@ -1085,7 +1244,7 @@ function formatSubredditInfo(subreddit) {
|
|
|
1085
1244
|
//#endregion
|
|
1086
1245
|
//#region src/index.ts
|
|
1087
1246
|
dotenv.config({ quiet: true });
|
|
1088
|
-
const VERSION = "1.
|
|
1247
|
+
const VERSION = "1.6.1";
|
|
1089
1248
|
function validateUserAgent(userAgent, username) {
|
|
1090
1249
|
if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
|
|
1091
1250
|
console.error("[Warning] User-Agent does not follow Reddit's recommended format");
|
|
@@ -1136,6 +1295,19 @@ function buildSafeModeConfig(safeMode) {
|
|
|
1136
1295
|
function unwrapClient() {
|
|
1137
1296
|
return getRedditClient().orThrow(/* @__PURE__ */ new Error("Reddit client not initialized"));
|
|
1138
1297
|
}
|
|
1298
|
+
function rssDisclaimer(source) {
|
|
1299
|
+
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." : "";
|
|
1300
|
+
}
|
|
1301
|
+
function formatPostSummary(post, index, source) {
|
|
1302
|
+
const statsLines = source === "rss" ? [] : [`- Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)`, `- Comments: ${post.stats.comments.toLocaleString()}`];
|
|
1303
|
+
return [
|
|
1304
|
+
`### ${index + 1}. ${post.title}`,
|
|
1305
|
+
`- Author: u/${post.author}`,
|
|
1306
|
+
...statsLines,
|
|
1307
|
+
`- Posted: ${post.metadata.posted}`,
|
|
1308
|
+
`- Link: ${post.links.shortLink}`
|
|
1309
|
+
].join("\n");
|
|
1310
|
+
}
|
|
1139
1311
|
function nextPageHint(after) {
|
|
1140
1312
|
return Option(after).fold(() => "", (cursor) => `\n\n---\nMore results available — call again with after="${cursor}" for the next page.`);
|
|
1141
1313
|
}
|
|
@@ -1208,9 +1380,12 @@ async function setupRedditClient() {
|
|
|
1208
1380
|
});
|
|
1209
1381
|
console.error("[Setup] Reddit client initialized");
|
|
1210
1382
|
console.error(`[Setup] Authentication mode: ${authMode}`);
|
|
1383
|
+
if (authMode === "anonymous") console.error("[Warning] REDDIT_AUTH_MODE=anonymous is deprecated; it is now an alias for auto without credentials.");
|
|
1211
1384
|
if (authMode === "anonymous" || !hasCredentials) {
|
|
1212
|
-
console.error("[Setup]
|
|
1213
|
-
console.error("[Setup]
|
|
1385
|
+
console.error("[Setup] RSS fallback mode — no OAuth credentials detected.");
|
|
1386
|
+
console.error("[Setup] Only browse_subreddit and get_top_posts are available (~1 req/min, no engagement metrics).");
|
|
1387
|
+
console.error("[Setup] Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET for full API access (100 req/min).");
|
|
1388
|
+
console.error("[Setup] See: https://www.reddit.com/wiki/api");
|
|
1214
1389
|
} else {
|
|
1215
1390
|
console.error("[Setup] Testing Reddit API connection...");
|
|
1216
1391
|
if (!await client.checkAuthentication()) {
|
|
@@ -1252,6 +1427,7 @@ Available capabilities:
|
|
|
1252
1427
|
- Search Reddit content across posts and subreddits
|
|
1253
1428
|
- Create posts and reply to posts/comments (with authentication)
|
|
1254
1429
|
- Edit your own posts and comments (with authentication)
|
|
1430
|
+
- Save or unsave posts and comments (with authentication)
|
|
1255
1431
|
- Delete your own posts and comments (with authentication)
|
|
1256
1432
|
- Analyze engagement metrics and community insights
|
|
1257
1433
|
|
|
@@ -1308,7 +1484,7 @@ Ready to handle Reddit API requests!`);
|
|
|
1308
1484
|
});
|
|
1309
1485
|
server.addTool({
|
|
1310
1486
|
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;
|
|
1487
|
+
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
1488
|
annotations: {
|
|
1313
1489
|
title: "Get User Info",
|
|
1314
1490
|
readOnlyHint: true,
|
|
@@ -1342,7 +1518,7 @@ server.addTool({
|
|
|
1342
1518
|
});
|
|
1343
1519
|
server.addTool({
|
|
1344
1520
|
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
|
|
1521
|
+
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
1522
|
annotations: {
|
|
1347
1523
|
title: "Get My Account",
|
|
1348
1524
|
readOnlyHint: true,
|
|
@@ -1412,7 +1588,7 @@ server.addTool({
|
|
|
1412
1588
|
});
|
|
1413
1589
|
server.addTool({
|
|
1414
1590
|
name: "get_user_posts",
|
|
1415
|
-
description: "Get posts submitted by a specific user, with sort (new/hot/top) and time filter. Read-only;
|
|
1591
|
+
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
1592
|
annotations: {
|
|
1417
1593
|
title: "Get User Posts",
|
|
1418
1594
|
readOnlyHint: true,
|
|
@@ -1464,7 +1640,7 @@ ${postSummaries}${nextPageHint(page.after)}`;
|
|
|
1464
1640
|
});
|
|
1465
1641
|
server.addTool({
|
|
1466
1642
|
name: "get_user_comments",
|
|
1467
|
-
description: "Get comments made by a specific user, with sort (new/hot/top) and time filter. Read-only;
|
|
1643
|
+
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
1644
|
annotations: {
|
|
1469
1645
|
title: "Get User Comments",
|
|
1470
1646
|
readOnlyHint: true,
|
|
@@ -1519,7 +1695,7 @@ ${commentSummaries}${nextPageHint(page.after)}`;
|
|
|
1519
1695
|
});
|
|
1520
1696
|
server.addTool({
|
|
1521
1697
|
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;
|
|
1698
|
+
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
1699
|
annotations: {
|
|
1524
1700
|
title: "Get Reddit Post",
|
|
1525
1701
|
readOnlyHint: true,
|
|
@@ -1568,7 +1744,7 @@ ${formattedPost.bestTimeToEngage}`;
|
|
|
1568
1744
|
});
|
|
1569
1745
|
server.addTool({
|
|
1570
1746
|
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
|
|
1747
|
+
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
1748
|
annotations: {
|
|
1573
1749
|
title: "Get Top Posts",
|
|
1574
1750
|
readOnlyHint: true,
|
|
@@ -1593,21 +1769,16 @@ server.addTool({
|
|
|
1593
1769
|
}, (page) => {
|
|
1594
1770
|
const posts = page.items;
|
|
1595
1771
|
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");
|
|
1772
|
+
const postSummaries = posts.map(formatPostInfo).map((post, index) => formatPostSummary(post, index, page.source)).join("\n\n");
|
|
1602
1773
|
return `# Top Posts from ${Option(args.subreddit).fold(() => "Home Feed", (sr) => `r/${sr}`)} (${args.time_filter})
|
|
1603
1774
|
|
|
1604
|
-
${postSummaries}${nextPageHint(page.after)}`;
|
|
1775
|
+
${postSummaries}${nextPageHint(page.after)}${rssDisclaimer(page.source)}`;
|
|
1605
1776
|
});
|
|
1606
1777
|
}
|
|
1607
1778
|
});
|
|
1608
1779
|
server.addTool({
|
|
1609
1780
|
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
|
|
1781
|
+
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
1782
|
annotations: {
|
|
1612
1783
|
title: "Browse Subreddit",
|
|
1613
1784
|
readOnlyHint: true,
|
|
@@ -1640,23 +1811,18 @@ server.addTool({
|
|
|
1640
1811
|
const posts = page.items;
|
|
1641
1812
|
const location = Option(args.subreddit).fold(() => "home feed", (sr) => `r/${sr}`);
|
|
1642
1813
|
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");
|
|
1814
|
+
const postSummaries = posts.map(formatPostInfo).map((post, index) => formatPostSummary(post, index, page.source)).join("\n\n");
|
|
1649
1815
|
const timeSuffix = args.sort === "top" || args.sort === "controversial" ? `, ${args.time_filter}` : "";
|
|
1650
1816
|
const heading = location === "home feed" ? "Home Feed" : location;
|
|
1651
1817
|
return `# ${args.sort} posts from ${heading} (${args.sort}${timeSuffix})
|
|
1652
1818
|
|
|
1653
|
-
${postSummaries}${nextPageHint(page.after)}`;
|
|
1819
|
+
${postSummaries}${nextPageHint(page.after)}${rssDisclaimer(page.source)}`;
|
|
1654
1820
|
});
|
|
1655
1821
|
}
|
|
1656
1822
|
});
|
|
1657
1823
|
server.addTool({
|
|
1658
1824
|
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;
|
|
1825
|
+
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
1826
|
annotations: {
|
|
1661
1827
|
title: "Get Subreddit Info",
|
|
1662
1828
|
readOnlyHint: true,
|
|
@@ -1700,7 +1866,7 @@ ${formattedSubreddit.description.full}
|
|
|
1700
1866
|
});
|
|
1701
1867
|
server.addTool({
|
|
1702
1868
|
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;
|
|
1869
|
+
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
1870
|
annotations: {
|
|
1705
1871
|
title: "Get Subreddit Rules",
|
|
1706
1872
|
readOnlyHint: true,
|
|
@@ -1725,7 +1891,7 @@ ${ruleList}`;
|
|
|
1725
1891
|
});
|
|
1726
1892
|
server.addTool({
|
|
1727
1893
|
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
|
|
1894
|
+
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
1895
|
annotations: {
|
|
1730
1896
|
title: "Get Post Flairs",
|
|
1731
1897
|
readOnlyHint: true,
|
|
@@ -1751,7 +1917,7 @@ Pass the desired \`flair_id\` to \`create_post\`.`;
|
|
|
1751
1917
|
});
|
|
1752
1918
|
server.addTool({
|
|
1753
1919
|
name: "get_trending_subreddits",
|
|
1754
|
-
description: "Get the subreddits Reddit is currently featuring as trending/popular. Read-only, no parameters;
|
|
1920
|
+
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
1921
|
annotations: {
|
|
1756
1922
|
title: "Get Trending Subreddits",
|
|
1757
1923
|
readOnlyHint: true,
|
|
@@ -1768,7 +1934,7 @@ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).j
|
|
|
1768
1934
|
});
|
|
1769
1935
|
server.addTool({
|
|
1770
1936
|
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;
|
|
1937
|
+
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
1938
|
annotations: {
|
|
1773
1939
|
title: "Search Reddit",
|
|
1774
1940
|
readOnlyHint: true,
|
|
@@ -2005,9 +2171,54 @@ The comment ${args.thing_id} has been updated with your new content.
|
|
|
2005
2171
|
**Note**: An "edited" marker will appear on your comment to show it has been modified.`);
|
|
2006
2172
|
}
|
|
2007
2173
|
});
|
|
2174
|
+
server.addTool({
|
|
2175
|
+
name: "save_content",
|
|
2176
|
+
description: "Save a post or comment to your account (a private bookmark, listed by get_my_saved). Mutating but idempotent — saving an already-saved item is a no-op. Requires REDDIT_USERNAME and REDDIT_PASSWORD; works on any post/comment you can see, not just your own. Accepts a full thing id (t3_ for a post, t1_ for a comment) — a bare id is treated as a post. Use unsave_content to undo.",
|
|
2177
|
+
annotations: {
|
|
2178
|
+
title: "Save Post or Comment",
|
|
2179
|
+
readOnlyHint: false,
|
|
2180
|
+
destructiveHint: false,
|
|
2181
|
+
idempotentHint: true,
|
|
2182
|
+
openWorldHint: true
|
|
2183
|
+
},
|
|
2184
|
+
parameters: z.object({
|
|
2185
|
+
thing_id: z.string().describe("The post or comment to save: a full thing id 't3_<id>' (post) or 't1_<id>' (comment). A bare id is treated as a post."),
|
|
2186
|
+
category: z.string().optional().describe("Optional save category to file this under (Reddit Premium feature; ignored on other accounts).")
|
|
2187
|
+
}),
|
|
2188
|
+
execute: async (args) => {
|
|
2189
|
+
const client = unwrapClient();
|
|
2190
|
+
if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
|
|
2191
|
+
return (await client.saveContent(args.thing_id, args.category)).fold((err) => {
|
|
2192
|
+
throw new Error(`Failed to save content: ${err.message}`);
|
|
2193
|
+
}, () => `# Content Saved Successfully
|
|
2194
|
+
|
|
2195
|
+
${args.thing_id} has been saved to your account. Use get_my_saved to view your saved items.`);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
server.addTool({
|
|
2199
|
+
name: "unsave_content",
|
|
2200
|
+
description: "Remove a post or comment from your saved items (undoes save_content). Mutating but idempotent — unsaving an item that isn't saved is a no-op. Requires REDDIT_USERNAME and REDDIT_PASSWORD. Accepts a full thing id (t3_ for a post, t1_ for a comment) — a bare id is treated as a post.",
|
|
2201
|
+
annotations: {
|
|
2202
|
+
title: "Unsave Post or Comment",
|
|
2203
|
+
readOnlyHint: false,
|
|
2204
|
+
destructiveHint: false,
|
|
2205
|
+
idempotentHint: true,
|
|
2206
|
+
openWorldHint: true
|
|
2207
|
+
},
|
|
2208
|
+
parameters: z.object({ thing_id: z.string().describe("The post or comment to unsave: a full thing id 't3_<id>' (post) or 't1_<id>' (comment). A bare id is treated as a post.") }),
|
|
2209
|
+
execute: async (args) => {
|
|
2210
|
+
const client = unwrapClient();
|
|
2211
|
+
if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
|
|
2212
|
+
return (await client.unsaveContent(args.thing_id)).fold((err) => {
|
|
2213
|
+
throw new Error(`Failed to unsave content: ${err.message}`);
|
|
2214
|
+
}, () => `# Content Unsaved Successfully
|
|
2215
|
+
|
|
2216
|
+
${args.thing_id} has been removed from your saved items.`);
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2008
2219
|
server.addTool({
|
|
2009
2220
|
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;
|
|
2221
|
+
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
2222
|
annotations: {
|
|
2012
2223
|
title: "Get Post Comments",
|
|
2013
2224
|
readOnlyHint: true,
|
|
@@ -2060,7 +2271,7 @@ ${comment.body}
|
|
|
2060
2271
|
});
|
|
2061
2272
|
server.addTool({
|
|
2062
2273
|
name: "get_more_comments",
|
|
2063
|
-
description: "Expand truncated 'load more comments' stubs in a thread. Read-only;
|
|
2274
|
+
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
2275
|
annotations: {
|
|
2065
2276
|
title: "Get More Comments",
|
|
2066
2277
|
readOnlyHint: true,
|