reddit-mcp-server 1.5.0 → 1.5.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/dist/bin.js CHANGED
@@ -3,12 +3,12 @@
3
3
  process.env.TRANSPORT_TYPE ??= "stdio";
4
4
  const args = process.argv.slice(2);
5
5
  if (args.includes("--version") || args.includes("-v")) {
6
- console.log("1.5.0");
6
+ console.log("1.5.1");
7
7
  process.exit(0);
8
8
  }
9
9
  if (args.includes("--help") || args.includes("-h")) {
10
10
  console.log(`
11
- Reddit MCP Server v1.5.0
11
+ Reddit MCP Server v1.5.1
12
12
 
13
13
  Usage: reddit-mcp-server [options]
14
14
 
package/dist/index.js CHANGED
@@ -992,7 +992,7 @@ function formatSubredditInfo(subreddit) {
992
992
  //#endregion
993
993
  //#region src/index.ts
994
994
  dotenv.config({ quiet: true });
995
- const VERSION = "1.5.0";
995
+ const VERSION = "1.5.1";
996
996
  function validateUserAgent(userAgent, username) {
997
997
  if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
998
998
  console.error("[Warning] User-Agent does not follow Reddit's recommended format");
@@ -1193,7 +1193,12 @@ For details: https://support.reddithelp.com/hc/en-us/articles/42728983564564-Res
1193
1193
  });
1194
1194
  server.addTool({
1195
1195
  name: "test_reddit_mcp_server",
1196
- description: "Test the Reddit MCP Server connection and configuration",
1196
+ description: "Health check for the Reddit MCP server. Read-only and side-effect-free — inspects local configuration only and makes no Reddit API calls. Returns the server version, whether the Reddit client is initialized, whether OAuth credentials are present, and whether write access (REDDIT_USERNAME/REDDIT_PASSWORD) is configured. Use this first to diagnose setup/auth problems. Do NOT use it to check Reddit's own status or connectivity — it never contacts Reddit. A \"✗ Write Access\" result means the write tools (create_post, reply_to_post, edit_*, delete_*) will fail.",
1197
+ annotations: {
1198
+ title: "Test Reddit MCP Server",
1199
+ readOnlyHint: true,
1200
+ openWorldHint: false
1201
+ },
1197
1202
  parameters: z.object({}),
1198
1203
  execute: () => {
1199
1204
  const client = getRedditClient();
@@ -1210,8 +1215,13 @@ Ready to handle Reddit API requests!`);
1210
1215
  });
1211
1216
  server.addTool({
1212
1217
  name: "get_user_info",
1213
- description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
1214
- parameters: z.object({ username: z.string().describe("The Reddit username (without u/ prefix)") }),
1218
+ 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; works in anonymous mode. 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.",
1219
+ annotations: {
1220
+ title: "Get User Info",
1221
+ readOnlyHint: true,
1222
+ openWorldHint: true
1223
+ },
1224
+ parameters: z.object({ username: z.string().describe("The target user's Reddit username, without the u/ prefix (e.g. 'spez', not 'u/spez').") }),
1215
1225
  execute: async (args) => {
1216
1226
  return (await unwrapClient().getUser(args.username)).fold((err) => {
1217
1227
  throw new Error(`Failed to get user info: ${err.message}`);
@@ -1239,7 +1249,12 @@ server.addTool({
1239
1249
  });
1240
1250
  server.addTool({
1241
1251
  name: "get_me",
1242
- description: "Get the authenticated user's own account info (karma, account status). Requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD); fails in anonymous mode.",
1252
+ 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 in anonymous mode. 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.",
1253
+ annotations: {
1254
+ title: "Get My Account",
1255
+ readOnlyHint: true,
1256
+ openWorldHint: true
1257
+ },
1243
1258
  parameters: z.object({}),
1244
1259
  execute: async () => {
1245
1260
  return (await unwrapClient().getMe()).fold((err) => {
@@ -1262,10 +1277,15 @@ server.addTool({
1262
1277
  });
1263
1278
  server.addTool({
1264
1279
  name: "get_my_overview",
1265
- description: "Get your own recent activity (posts and comments combined). Requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD).",
1280
+ description: "Get the authenticated user's own recent activity posts and comments interleaved, newest first. Read-only but requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD). Returns up to `limit` items plus an `after` cursor for the next page. Use get_my_saved for saved items, or get_user_posts / get_user_comments for another user. Do NOT use this to fetch a specific post's thread — use get_post_comments.",
1281
+ annotations: {
1282
+ title: "Get My Overview",
1283
+ readOnlyHint: true,
1284
+ openWorldHint: true
1285
+ },
1266
1286
  parameters: z.object({
1267
- limit: z.number().min(1).max(100).default(25).describe("Number of items to retrieve"),
1268
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page")
1287
+ limit: z.number().min(1).max(100).default(25).describe("How many activity items to return, 1–100 (default 25)."),
1288
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value returned by a previous call. Omit for the first page.")
1269
1289
  }),
1270
1290
  execute: async (args) => {
1271
1291
  return (await unwrapClient().getMyOverview({
@@ -1278,10 +1298,15 @@ server.addTool({
1278
1298
  });
1279
1299
  server.addTool({
1280
1300
  name: "get_my_saved",
1281
- description: "Get your saved posts and comments. Requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD); saved content is private.",
1301
+ description: "Get the authenticated user's saved posts and comments (private to the account). Read-only but requires user credentials (REDDIT_USERNAME/REDDIT_PASSWORD). Returns up to `limit` items plus an `after` pagination cursor. Use get_my_overview for your authored activity. Do NOT use this for another user — saved items are private and have no cross-user equivalent.",
1302
+ annotations: {
1303
+ title: "Get My Saved",
1304
+ readOnlyHint: true,
1305
+ openWorldHint: true
1306
+ },
1282
1307
  parameters: z.object({
1283
- limit: z.number().min(1).max(100).default(25).describe("Number of items to retrieve"),
1284
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page")
1308
+ limit: z.number().min(1).max(100).default(25).describe("How many saved items to return, 1–100 (default 25)."),
1309
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value returned by a previous call. Omit for the first page.")
1285
1310
  }),
1286
1311
  execute: async (args) => {
1287
1312
  return (await unwrapClient().getMySaved({
@@ -1294,14 +1319,19 @@ server.addTool({
1294
1319
  });
1295
1320
  server.addTool({
1296
1321
  name: "get_user_posts",
1297
- description: "Get recent posts by a Reddit user with sorting and filtering options",
1322
+ description: "Get posts submitted by a specific user, with sort (new/hot/top) and time filter. Read-only; works anonymously. 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.",
1323
+ annotations: {
1324
+ title: "Get User Posts",
1325
+ readOnlyHint: true,
1326
+ openWorldHint: true
1327
+ },
1298
1328
  parameters: z.object({
1299
- username: z.string().describe("The Reddit username (without u/ prefix)"),
1329
+ username: z.string().describe("The author's Reddit username, without the u/ prefix (e.g. 'spez')."),
1300
1330
  sort: z.enum([
1301
1331
  "new",
1302
1332
  "hot",
1303
1333
  "top"
1304
- ]).default("new").describe("Sort order for posts"),
1334
+ ]).default("new").describe("Ordering: 'new' (most recent), 'hot' (currently active), or 'top' (highest score within `time_filter`). Default 'new'."),
1305
1335
  time_filter: z.enum([
1306
1336
  "hour",
1307
1337
  "day",
@@ -1309,9 +1339,9 @@ server.addTool({
1309
1339
  "month",
1310
1340
  "year",
1311
1341
  "all"
1312
- ]).default("all").describe("Time filter for top posts"),
1313
- limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve"),
1314
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page to fetch the next page")
1342
+ ]).default("all").describe("Time window for scoring; only applies when sort='top'. Ignored for 'new'/'hot'. Default 'all'."),
1343
+ limit: z.number().min(1).max(100).default(10).describe("How many posts to return, 1–100 (default 10)."),
1344
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value from a previous call. Omit for the first page.")
1315
1345
  }),
1316
1346
  execute: async (args) => {
1317
1347
  return (await unwrapClient().getUserPosts(args.username, {
@@ -1341,14 +1371,19 @@ ${postSummaries}${nextPageHint(page.after)}`;
1341
1371
  });
1342
1372
  server.addTool({
1343
1373
  name: "get_user_comments",
1344
- description: "Get recent comments by a Reddit user with sorting and filtering options",
1374
+ description: "Get comments made by a specific user, with sort (new/hot/top) and time filter. Read-only; works anonymously. 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.",
1375
+ annotations: {
1376
+ title: "Get User Comments",
1377
+ readOnlyHint: true,
1378
+ openWorldHint: true
1379
+ },
1345
1380
  parameters: z.object({
1346
- username: z.string().describe("The Reddit username (without u/ prefix)"),
1381
+ username: z.string().describe("The author's Reddit username, without the u/ prefix (e.g. 'spez')."),
1347
1382
  sort: z.enum([
1348
1383
  "new",
1349
1384
  "hot",
1350
1385
  "top"
1351
- ]).default("new").describe("Sort order for comments"),
1386
+ ]).default("new").describe("Ordering: 'new' (most recent), 'hot' (currently active), or 'top' (highest score within `time_filter`). Default 'new'."),
1352
1387
  time_filter: z.enum([
1353
1388
  "hour",
1354
1389
  "day",
@@ -1356,9 +1391,9 @@ server.addTool({
1356
1391
  "month",
1357
1392
  "year",
1358
1393
  "all"
1359
- ]).default("all").describe("Time filter for top comments"),
1360
- limit: z.number().min(1).max(100).default(10).describe("Number of comments to retrieve"),
1361
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page to fetch the next page")
1394
+ ]).default("all").describe("Time window for scoring; only applies when sort='top'. Ignored for 'new'/'hot'. Default 'all'."),
1395
+ limit: z.number().min(1).max(100).default(10).describe("How many comments to return, 1–100 (default 10)."),
1396
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value from a previous call. Omit for the first page.")
1362
1397
  }),
1363
1398
  execute: async (args) => {
1364
1399
  return (await unwrapClient().getUserComments(args.username, {
@@ -1391,10 +1426,15 @@ ${commentSummaries}${nextPageHint(page.after)}`;
1391
1426
  });
1392
1427
  server.addTool({
1393
1428
  name: "get_reddit_post",
1394
- description: "Get detailed information about a specific Reddit post including content, stats, and engagement analysis",
1429
+ 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; works anonymously. 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).",
1430
+ annotations: {
1431
+ title: "Get Reddit Post",
1432
+ readOnlyHint: true,
1433
+ openWorldHint: true
1434
+ },
1395
1435
  parameters: z.object({
1396
- subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
1397
- post_id: z.string().describe("The Reddit post ID")
1436
+ subreddit: z.string().describe("The subreddit the post lives in, without the r/ prefix (e.g. 'programming')."),
1437
+ post_id: z.string().describe("Base36 post id — the segment after /comments/ in a permalink like reddit.com/r/<sub>/comments/<post_id>/... (e.g. '1abc23'). With or without a t3_ prefix.")
1398
1438
  }),
1399
1439
  execute: async (args) => {
1400
1440
  return (await unwrapClient().getPost(args.post_id, args.subreddit)).fold((err) => {
@@ -1435,9 +1475,14 @@ ${formattedPost.bestTimeToEngage}`;
1435
1475
  });
1436
1476
  server.addTool({
1437
1477
  name: "get_top_posts",
1438
- description: "Get top posts from a subreddit or from the Reddit home feed",
1478
+ 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 anonymously. Returns a page of posts (title, author, score, upvote ratio, comments, link) 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.",
1479
+ annotations: {
1480
+ title: "Get Top Posts",
1481
+ readOnlyHint: true,
1482
+ openWorldHint: true
1483
+ },
1439
1484
  parameters: z.object({
1440
- subreddit: z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
1485
+ subreddit: z.string().optional().describe("Subreddit to read, without the r/ prefix (e.g. 'science'). Omit to use the authenticated home feed (requires credentials)."),
1441
1486
  time_filter: z.enum([
1442
1487
  "hour",
1443
1488
  "day",
@@ -1445,9 +1490,9 @@ server.addTool({
1445
1490
  "month",
1446
1491
  "year",
1447
1492
  "all"
1448
- ]).default("week").describe("Time period for top posts"),
1449
- limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve"),
1450
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page to fetch the next page")
1493
+ ]).default("week").describe("Time window the 'top' ranking is computed over (e.g. 'day' = top today). Default 'week'."),
1494
+ limit: z.number().min(1).max(100).default(10).describe("How many posts to return, 1–100 (default 10)."),
1495
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value from a previous call. Omit for the first page.")
1451
1496
  }),
1452
1497
  execute: async (args) => {
1453
1498
  return (await unwrapClient().getTopPosts(args.subreddit ?? "", args.time_filter, args.limit, args.after)).fold((err) => {
@@ -1469,16 +1514,21 @@ ${postSummaries}${nextPageHint(page.after)}`;
1469
1514
  });
1470
1515
  server.addTool({
1471
1516
  name: "browse_subreddit",
1472
- description: "Browse posts from a subreddit or the Reddit home feed with a sort order (hot, new, top, rising, controversial). The time_filter only applies to top and controversial sorts.",
1517
+ 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 anonymously. `time_filter` applies only to the top and controversial sorts. Returns a page of posts (title, author, score, upvote ratio, comments, link) 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.",
1518
+ annotations: {
1519
+ title: "Browse Subreddit",
1520
+ readOnlyHint: true,
1521
+ openWorldHint: true
1522
+ },
1473
1523
  parameters: z.object({
1474
- subreddit: z.string().optional().describe("The subreddit name (without r/ prefix). Leave empty for home feed"),
1524
+ subreddit: z.string().optional().describe("Subreddit to browse, without the r/ prefix (e.g. 'news'). Omit to use the authenticated home feed (requires credentials)."),
1475
1525
  sort: z.enum([
1476
1526
  "hot",
1477
1527
  "new",
1478
1528
  "top",
1479
1529
  "rising",
1480
1530
  "controversial"
1481
- ]).default("hot").describe("Sort order for posts"),
1531
+ ]).default("hot").describe("Feed ordering: 'hot' (default), 'new', 'rising', 'top', or 'controversial'. 'top'/'controversial' honor `time_filter`."),
1482
1532
  time_filter: z.enum([
1483
1533
  "hour",
1484
1534
  "day",
@@ -1486,9 +1536,9 @@ server.addTool({
1486
1536
  "month",
1487
1537
  "year",
1488
1538
  "all"
1489
- ]).default("week").describe("Time period (only applies to top and controversial sorts)"),
1490
- limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve"),
1491
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page to fetch the next page")
1539
+ ]).default("week").describe("Time window; only applies to sort='top' or 'controversial'. Ignored otherwise. Default 'week'."),
1540
+ limit: z.number().min(1).max(100).default(10).describe("How many posts to return, 1–100 (default 10)."),
1541
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value from a previous call. Omit for the first page.")
1492
1542
  }),
1493
1543
  execute: async (args) => {
1494
1544
  return (await unwrapClient().browseSubreddit(args.subreddit ?? "", args.sort, args.time_filter, args.limit, args.after)).fold((err) => {
@@ -1513,8 +1563,13 @@ ${postSummaries}${nextPageHint(page.after)}`;
1513
1563
  });
1514
1564
  server.addTool({
1515
1565
  name: "get_subreddit_info",
1516
- description: "Get detailed information about a subreddit including description, stats, and community analysis",
1517
- parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
1566
+ 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; works anonymously. 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'.",
1567
+ annotations: {
1568
+ title: "Get Subreddit Info",
1569
+ readOnlyHint: true,
1570
+ openWorldHint: true
1571
+ },
1572
+ parameters: z.object({ subreddit_name: z.string().describe("The subreddit name, without the r/ prefix (e.g. 'askscience').") }),
1518
1573
  execute: async (args) => {
1519
1574
  return (await unwrapClient().getSubredditInfo(args.subreddit_name)).fold((err) => {
1520
1575
  throw new Error(`Failed to get subreddit info: ${err.message}`);
@@ -1552,8 +1607,13 @@ ${formattedSubreddit.description.full}
1552
1607
  });
1553
1608
  server.addTool({
1554
1609
  name: "get_subreddit_rules",
1555
- description: "Get a subreddit's posting rules. Useful to check requirements before creating a post to avoid auto-removal.",
1556
- parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
1610
+ description: "Get a subreddit's posting rules (each rule's name, what it applies to, and its description). Read-only; works anonymously. 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.",
1611
+ annotations: {
1612
+ title: "Get Subreddit Rules",
1613
+ readOnlyHint: true,
1614
+ openWorldHint: true
1615
+ },
1616
+ parameters: z.object({ subreddit_name: z.string().describe("The subreddit name, without the r/ prefix (e.g. 'AskReddit').") }),
1557
1617
  execute: async (args) => {
1558
1618
  return (await unwrapClient().getSubredditRules(args.subreddit_name)).fold((err) => {
1559
1619
  throw new Error(`Failed to get subreddit rules: ${err.message}`);
@@ -1572,8 +1632,13 @@ ${ruleList}`;
1572
1632
  });
1573
1633
  server.addTool({
1574
1634
  name: "get_post_flairs",
1575
- description: "List the available link flairs for a subreddit (use a flair_id with create_post). Requires user credentials; many subreddits only expose flairs to members, so this may fail in anonymous mode.",
1576
- parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
1635
+ 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 anonymously. 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.",
1636
+ annotations: {
1637
+ title: "Get Post Flairs",
1638
+ readOnlyHint: true,
1639
+ openWorldHint: true
1640
+ },
1641
+ parameters: z.object({ subreddit_name: z.string().describe("The subreddit name, without the r/ prefix (e.g. 'gadgets').") }),
1577
1642
  execute: async (args) => {
1578
1643
  return (await unwrapClient().getPostFlairs(args.subreddit_name)).fold((err) => {
1579
1644
  throw new Error(`Failed to get post flairs: ${err.message}`);
@@ -1593,7 +1658,12 @@ Pass the desired \`flair_id\` to \`create_post\`.`;
1593
1658
  });
1594
1659
  server.addTool({
1595
1660
  name: "get_trending_subreddits",
1596
- description: "Get a list of currently trending subreddits",
1661
+ description: "Get the subreddits Reddit is currently featuring as trending/popular. Read-only, no parameters; works anonymously. 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'.",
1662
+ annotations: {
1663
+ title: "Get Trending Subreddits",
1664
+ readOnlyHint: true,
1665
+ openWorldHint: true
1666
+ },
1597
1667
  parameters: z.object({}),
1598
1668
  execute: async () => {
1599
1669
  return (await unwrapClient().getTrendingSubreddits()).fold((err) => {
@@ -1605,10 +1675,15 @@ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).j
1605
1675
  });
1606
1676
  server.addTool({
1607
1677
  name: "search_reddit",
1608
- description: "Search Reddit for posts and content across subreddits",
1678
+ description: "Search Reddit for posts — or subreddits/users via `type` — optionally scoped to one subreddit, with sort and time filters. Read-only; works anonymously. 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.",
1679
+ annotations: {
1680
+ title: "Search Reddit",
1681
+ readOnlyHint: true,
1682
+ openWorldHint: true
1683
+ },
1609
1684
  parameters: z.object({
1610
- query: z.string().describe("Search query"),
1611
- subreddit: z.string().optional().describe("Limit search to specific subreddit (without r/ prefix)"),
1685
+ query: z.string().describe("Search terms; supports Reddit operators (quotes for exact phrases, author:name, self:yes). Must be non-empty."),
1686
+ subreddit: z.string().optional().describe("Restrict results to this subreddit, without the r/ prefix (e.g. 'python'). Omit to search all of Reddit."),
1612
1687
  sort: z.enum([
1613
1688
  "relevance",
1614
1689
  "hot",
@@ -1623,14 +1698,14 @@ server.addTool({
1623
1698
  "month",
1624
1699
  "year",
1625
1700
  "all"
1626
- ]).default("all").describe("Time filter"),
1627
- limit: z.number().min(1).max(100).default(10).describe("Number of results"),
1701
+ ]).default("all").describe("Restrict to results from this recent window (e.g. 'week'). Default 'all' (no time limit)."),
1702
+ limit: z.number().min(1).max(100).default(10).describe("How many results to return, 1–100 (default 10)."),
1628
1703
  type: z.enum([
1629
1704
  "link",
1630
1705
  "sr",
1631
1706
  "user"
1632
- ]).default("link").describe("Type of content to search"),
1633
- after: z.string().optional().describe("Pagination cursor: pass the `after` value from a previous page to fetch the next page")
1707
+ ]).default("link").describe("What to search for: 'link' = posts (default), 'sr' = subreddits, 'user' = users."),
1708
+ after: z.string().optional().describe("Forward pagination cursor: the `after` value from a previous call. Omit for the first page.")
1634
1709
  }),
1635
1710
  execute: async (args) => {
1636
1711
  const client = unwrapClient();
@@ -1671,14 +1746,21 @@ ${searchResults}${nextPageHint(page.after)}`;
1671
1746
  });
1672
1747
  server.addTool({
1673
1748
  name: "create_post",
1674
- description: "Create a new post in a subreddit (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: Rapid posting or duplicate content may trigger Reddit's spam detection and result in account bans. Consider enabling REDDIT_SAFE_MODE=standard for protection.",
1749
+ description: "Create a new text or link post in a subreddit. Mutating and NOT idempotent — each call publishes a separate post. Requires REDDIT_USERNAME and REDDIT_PASSWORD; fails without them. Returns the new post's id and URL. Check get_subreddit_rules and get_post_flairs first, since many subreddits require a flair or reject certain content. WARNING: rapid posting or duplicate content may trigger Reddit's spam detection and account bans enable REDDIT_SAFE_MODE=standard for rate limiting and duplicate detection.",
1750
+ annotations: {
1751
+ title: "Create Post",
1752
+ readOnlyHint: false,
1753
+ destructiveHint: false,
1754
+ idempotentHint: false,
1755
+ openWorldHint: true
1756
+ },
1675
1757
  parameters: z.object({
1676
- subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
1677
- title: z.string().describe("The post title"),
1678
- content: z.string().describe("The post content (text for self posts, URL for link posts)"),
1679
- is_self: z.boolean().default(true).describe("Whether this is a self post (text) or link post"),
1680
- flair_id: z.string().optional().describe("Link flair template id (from get_post_flairs); many subreddits require a flair"),
1681
- flair_text: z.string().optional().describe("Custom flair text, only for flairs whose template is text-editable")
1758
+ subreddit: z.string().describe("Target subreddit, without the r/ prefix (e.g. 'test')."),
1759
+ title: z.string().describe("Post title (cannot be edited after creation)."),
1760
+ content: z.string().describe("For a self post (is_self=true): the body text, Reddit markdown supported. For a link post (is_self=false): the destination URL."),
1761
+ is_self: z.boolean().default(true).describe("true = text/self post using `content` as the body (default); false = link post using `content` as the URL."),
1762
+ flair_id: z.string().optional().describe("Link flair template id from get_post_flairs; many subreddits require one or the post is auto-removed."),
1763
+ flair_text: z.string().optional().describe("Custom flair text, allowed only for flairs whose template is text-editable.")
1682
1764
  }),
1683
1765
  execute: async (args) => {
1684
1766
  const client = unwrapClient();
@@ -1701,10 +1783,17 @@ Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
1701
1783
  });
1702
1784
  server.addTool({
1703
1785
  name: "reply_to_post",
1704
- description: "Post a reply to an existing Reddit post or comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: Rapid commenting or duplicate content may trigger Reddit's spam detection. Enable REDDIT_SAFE_MODE=standard for rate limiting and duplicate detection.",
1786
+ description: "Post a reply to an existing post or comment. Mutating and NOT idempotent — each call adds a new comment. Requires REDDIT_USERNAME and REDDIT_PASSWORD. The parent is identified by its thing id — t3_ for a post, t1_ for a comment — so this creates both top-level and nested replies. Returns the new comment's id. Use edit_comment to change a reply you already posted. WARNING: rapid or duplicate replies may trigger Reddit's spam detection; enable REDDIT_SAFE_MODE=standard for rate limiting and duplicate detection.",
1787
+ annotations: {
1788
+ title: "Reply to Post or Comment",
1789
+ readOnlyHint: false,
1790
+ destructiveHint: false,
1791
+ idempotentHint: false,
1792
+ openWorldHint: true
1793
+ },
1705
1794
  parameters: z.object({
1706
- post_id: z.string().describe("The Reddit post ID (thing_id, e.g., t3_xxxxx for posts, t1_xxxxx for comments)"),
1707
- content: z.string().describe("The reply content")
1795
+ post_id: z.string().describe("Parent thing id to reply under: t3_<id> for a post (creates a top-level comment) or t1_<id> for a comment (creates a nested reply)."),
1796
+ content: z.string().describe("Reply body text; Reddit markdown supported.")
1708
1797
  }),
1709
1798
  execute: async (args) => {
1710
1799
  const client = unwrapClient();
@@ -1723,8 +1812,15 @@ Your reply has been successfully posted.`);
1723
1812
  });
1724
1813
  server.addTool({
1725
1814
  name: "delete_post",
1726
- description: "Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1727
- parameters: z.object({ thing_id: z.string().describe("The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing.") }),
1815
+ description: "Permanently delete one of your own posts. Mutating and destructive but idempotent — deleting an already-deleted post is a no-op. Requires REDDIT_USERNAME and REDDIT_PASSWORD, and only works on posts authored by the authenticated account. Only affects the post you name; use delete_comment for comments. WARNING: this cannot be undone — the content is removed, though the post id remains.",
1816
+ annotations: {
1817
+ title: "Delete Post",
1818
+ readOnlyHint: false,
1819
+ destructiveHint: true,
1820
+ idempotentHint: true,
1821
+ openWorldHint: true
1822
+ },
1823
+ parameters: z.object({ thing_id: z.string().describe("The post to delete: a full thing id 't3_<id>' or just the base36 post id '<id>' (the 't3_' prefix is added automatically). Must be a post you authored.") }),
1728
1824
  execute: async (args) => {
1729
1825
  const client = unwrapClient();
1730
1826
  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.");
@@ -1739,8 +1835,15 @@ The post ${args.thing_id} has been permanently deleted from Reddit.
1739
1835
  });
1740
1836
  server.addTool({
1741
1837
  name: "delete_comment",
1742
- description: "Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1743
- parameters: z.object({ thing_id: z.string().describe("The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing.") }),
1838
+ description: "Permanently delete one of your own comments. Mutating and destructive but idempotent — deleting an already-deleted comment is a no-op. Requires REDDIT_USERNAME and REDDIT_PASSWORD, and only works on comments authored by the authenticated account. Only affects the comment you name; use delete_post for posts. WARNING: this cannot be undone — the content is removed, though the comment id remains.",
1839
+ annotations: {
1840
+ title: "Delete Comment",
1841
+ readOnlyHint: false,
1842
+ destructiveHint: true,
1843
+ idempotentHint: true,
1844
+ openWorldHint: true
1845
+ },
1846
+ parameters: z.object({ thing_id: z.string().describe("The comment to delete: a full thing id 't1_<id>' or just the base36 comment id '<id>' (the 't1_' prefix is added automatically). Must be a comment you authored.") }),
1744
1847
  execute: async (args) => {
1745
1848
  const client = unwrapClient();
1746
1849
  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.");
@@ -1755,10 +1858,17 @@ The comment ${args.thing_id} has been permanently deleted from Reddit.
1755
1858
  });
1756
1859
  server.addTool({
1757
1860
  name: "edit_post",
1758
- 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. WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
1861
+ description: "Replace the body text of one of your own self-text posts. Mutating and idempotent (same text → same result); it overwrites the previous body. Requires REDDIT_USERNAME and REDDIT_PASSWORD, and works only on self posts you authored — titles and link posts cannot be edited. Adds an \"edited\" marker. Use create_post to make a new post, or edit_comment for comments. WARNING: rapid edits may trigger spam detection; enable REDDIT_SAFE_MODE for protection.",
1862
+ annotations: {
1863
+ title: "Edit Post",
1864
+ readOnlyHint: false,
1865
+ destructiveHint: true,
1866
+ idempotentHint: true,
1867
+ openWorldHint: true
1868
+ },
1759
1869
  parameters: z.object({
1760
- thing_id: z.string().describe("The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing."),
1761
- new_text: z.string().describe("The new text content for the post. Supports Reddit markdown formatting.")
1870
+ thing_id: z.string().describe("The post to edit: a full thing id 't3_<id>' or just the base36 post id '<id>' (the 't3_' prefix is added automatically). Must be a self-text post you authored."),
1871
+ new_text: z.string().describe("Replacement body text; fully overwrites the current body. Reddit markdown supported.")
1762
1872
  }),
1763
1873
  execute: async (args) => {
1764
1874
  const client = unwrapClient();
@@ -1778,10 +1888,17 @@ The post ${args.thing_id} has been updated with your new content.
1778
1888
  });
1779
1889
  server.addTool({
1780
1890
  name: "edit_comment",
1781
- description: "Edit your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). Update the text content of a comment you previously posted. WARNING: Rapid edits may trigger spam detection. Enable REDDIT_SAFE_MODE for protection.",
1891
+ description: "Replace the text of one of your own comments. Mutating and idempotent (same text → same result); it overwrites the previous content. Requires REDDIT_USERNAME and REDDIT_PASSWORD, and works only on comments you authored. Adds an \"edited\" marker. Use reply_to_post to add a new comment, or edit_post for posts. WARNING: rapid edits may trigger spam detection; enable REDDIT_SAFE_MODE for protection.",
1892
+ annotations: {
1893
+ title: "Edit Comment",
1894
+ readOnlyHint: false,
1895
+ destructiveHint: true,
1896
+ idempotentHint: true,
1897
+ openWorldHint: true
1898
+ },
1782
1899
  parameters: z.object({
1783
- thing_id: z.string().describe("The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing."),
1784
- new_text: z.string().describe("The new text content for the comment. Supports Reddit markdown formatting.")
1900
+ thing_id: z.string().describe("The comment to edit: a full thing id 't1_<id>' or just the base36 comment id '<id>' (the 't1_' prefix is added automatically). Must be a comment you authored."),
1901
+ new_text: z.string().describe("Replacement comment text; fully overwrites the current content. Reddit markdown supported.")
1785
1902
  }),
1786
1903
  execute: async (args) => {
1787
1904
  const client = unwrapClient();
@@ -1797,10 +1914,15 @@ The comment ${args.thing_id} has been updated with your new content.
1797
1914
  });
1798
1915
  server.addTool({
1799
1916
  name: "get_post_comments",
1800
- description: "Get comments from a specific Reddit post",
1917
+ description: "Get the comment thread for a post (by post id + subreddit), sorted best/top/new/controversial/old/qa. Read-only; works anonymously. 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.",
1918
+ annotations: {
1919
+ title: "Get Post Comments",
1920
+ readOnlyHint: true,
1921
+ openWorldHint: true
1922
+ },
1801
1923
  parameters: z.object({
1802
- post_id: z.string().describe("The Reddit post ID"),
1803
- subreddit: z.string().describe("The subreddit name (without r/ prefix)"),
1924
+ post_id: z.string().describe("Base36 post id — the segment after /comments/ in a permalink (e.g. '1abc23'). With or without a t3_ prefix."),
1925
+ subreddit: z.string().describe("The subreddit the post lives in, without the r/ prefix (e.g. 'movies')."),
1804
1926
  sort: z.enum([
1805
1927
  "best",
1806
1928
  "top",
@@ -1808,8 +1930,8 @@ server.addTool({
1808
1930
  "controversial",
1809
1931
  "old",
1810
1932
  "qa"
1811
- ]).default("best").describe("Comment sort order"),
1812
- limit: z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
1933
+ ]).default("best").describe("Comment ordering: 'best' (default), 'top', 'new', 'controversial', 'old', or 'qa' (Q&A)."),
1934
+ limit: z.number().min(1).max(500).default(100).describe("Maximum comments to return, 1–500 (default 100). Deeply nested replies may still be truncated as 'load more' stubs.")
1813
1935
  }),
1814
1936
  execute: async (args) => {
1815
1937
  const client = unwrapClient();
@@ -1845,10 +1967,15 @@ ${comment.body}
1845
1967
  });
1846
1968
  server.addTool({
1847
1969
  name: "get_more_comments",
1848
- description: "Expand truncated 'load more comments' stubs in a thread. Pass the post's link id and the comment ids from a 'more' node (returned by get_post_comments) to fetch those comments.",
1970
+ description: "Expand truncated 'load more comments' stubs in a thread. Read-only; works anonymously. 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.",
1971
+ annotations: {
1972
+ title: "Get More Comments",
1973
+ readOnlyHint: true,
1974
+ openWorldHint: true
1975
+ },
1849
1976
  parameters: z.object({
1850
- link_id: z.string().describe("The post (link) id, with or without the t3_ prefix"),
1851
- comment_ids: z.array(z.string()).min(1).describe("Comment ids to expand (from a 'more' node)")
1977
+ link_id: z.string().describe("The parent post's link id (base36, with or without the t3_ prefix) that the stub belongs to."),
1978
+ comment_ids: z.array(z.string()).min(1).describe("Base36 comment ids to expand, taken from a 'more' node returned by get_post_comments (not arbitrary ids).")
1852
1979
  }),
1853
1980
  execute: async (args) => {
1854
1981
  return (await unwrapClient().getMoreComments(args.link_id, args.comment_ids)).fold((err) => {