redditapis-mcp
Official MCP server for redditapis.com. Search Reddit posts, comments, communities, users and media; read subreddits, comment trees, top posts and profiles; create and manage monitors and webhooks. Native tools for Claude, Cursor and any MCP client.
npm · fingerprint 3aaa043e2cc5b6cdade7ecc9 · repository · RSS
What changed
Changed 2026-10-17 10:50 UTC
46 schema or annotation changes with the description left byte-identical, 1 tool added.
--- pinned/reddit_account_me +++ observed/reddit_account_me { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_account_me" + "title": "Get Account Balance" }, "description": "Returns the remaining credit balance and usage totals of the account behind this API key. Free: not metered, and costs no credit. A 402 from a metered call means the balance is exhausted; that response carries a top-up URL.", "execution": { - "name": "reddit_account_me" + "name": "reddit_account_me", + "title": "Get Account Balance" }
+ tool added: reddit_account_payments
--- pinned/reddit_by_id +++ observed/reddit_by_id { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_by_id" + "title": "Get Posts by ID" }, "description": "Bulk-fetches posts by their t3_ fullnames in ONE call (up to 100), in place of a request per post. Takes a comma-separated list of fullnames from a search or listing. Returns posts with title, author, score, comment count, and permalink, the same post shape as the listing endpoints. The result is not necessarily one-to-one with the request, and `meta` reports which: `listing_status` is `complete` only when every fullname came back, `truncated` is a boolean, and `missing_fullnames` names exactly which ids did not come back. Example: fullnames='t3_abc123,t3_def456'.", "execution": { - "name": "reddit_by_id" + "name": "reddit_by_id", + "title": "Get Posts by ID" }
--- pinned/reddit_deep_comment_search +++ observed/reddit_deep_comment_search { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_deep_comment_search" + "title": "Deep Comment Search" }, "description": "Comment search that returns the ACTUAL comments whose body matches the keyword, sorted by score (highest first), with body, score, author, a comment-deep permalink, and the parent post. It fetches each matching post's comment tree and filters the comment bodies, so the results are first-hand opinions and answers rather than parent posts. Premium call (it fans out into several reads): `limit` sets how many parent POSTS to expand, 1-25 (default 5), not how many comments come back. The response's `after` cursor expands the NEXT batch of parent posts. `max_comments` optionally caps how many comments come back (the top-scored are kept). Matching is on the visible comment text at word boundaries (link URLs are ignored), so every result mentions the query where a reader can see it. Best-effort: a deleted or deeply-nested comment may be missed (meta.truncated flags when a tree was too deep). Each parent post is its own comment-tree read and one can fail while the others succeed: meta.partial is true with meta.trees_failed and meta.trees_fetched when that happened, so the result covers fewer parent posts than it scanned; the call fails only when every tree failed and at least one was refused upstream. group_by='author' switches to a research mode that returns WHO is talking about the query (distinct people ranked by matching-comment count) in place of a flat comment list, capped by max_authors. Example: q='best mechanical keyboard' sort='relevance' t='year'.", "execution": { - "name": "reddit_deep_comment_search" + "name": "reddit_deep_comment_search", + "title": "Deep Comment Search" }
--- pinned/reddit_explain +++ observed/reddit_explain { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_explain" + "title": "Explain Fetched Rows" }, "description": "Returns the provenance of the rows this server has already fetched in this session: per completed call, the tool that made it, the endpoint it reached, the timestamp the response arrived, how many seconds old that copy now is, the upstream request id and the response size. Provenance is recorded per call, so every row in one response shares it. The redditapis.com read path holds no response cache for customer reads, so each call is served live at the moment it runs and the age reported here is the age of this session's copy of the rows rather than the age of a stored row; `response_cache` says so in the result. The ledger holds the 20 most recent completed calls, lives in memory, starts empty, is scoped to this server and carries no other caller's calls. How long each category of data is kept is published at https://www.redditapis.com/privacy-and-data-handling.", "execution": { - "name": "reddit_explain" + "name": "reddit_explain", + "title": "Explain Fetched Rows" }
--- pinned/reddit_feedback_get +++ observed/reddit_feedback_get { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_feedback_get" + "title": "Get Feedback Report" }, "description": "Returns the status of a feedback report this account sent earlier, by its server id: status new, triaged, shipped or declined, the team's response text if any, and updated_at, which moves only when the team acts on it. Free per call. 404 if the id is not on this account.", "execution": { - "name": "reddit_feedback_get" + "name": "reddit_feedback_get", + "title": "Get Feedback Report" }
--- pinned/reddit_feedback_list +++ observed/reddit_feedback_list { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_feedback_list" + "title": "List Feedback Reports" }, "description": "Lists the feedback reports this account has sent, newest first, with each one's current status. The server id is returned only once, when a report is sent, so this listing is how a report whose id was not kept is found again; it also shows whether a report landed and whether the team has acted on it. Optional filters by status or type; paging uses the cursor from a previous response. Free per call, not metered. Returns {feedback: [...], count, limit, next_cursor}; pages continue while next_cursor is non-null, and an account that has filed nothing gets an empty list and a 200, not an error. It lists SENT reports on the server, which differ from the unsent local drafts on this machine.", "execution": { - "name": "reddit_feedback_list" + "name": "reddit_feedback_list", + "title": "List Feedback Reports" }
--- pinned/reddit_feedback_send +++ observed/reddit_feedback_send { "annotations": { - "destructiveHint": false, - "openWorldHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_feedback_send" + "title": "Send Feedback Report" }, "description": "Reports a product problem or gap in redditapis.com to its team, through a local draft queue. action \"draft\" (the default) writes a report to a queue on this machine and sends nothing; it makes no network call. action \"list\" returns the pending drafts with their ids. action \"send\" posts the named draft ids to POST /feedback (free, not metered) and returns a server id per report. action \"discard\" drops the named drafts. The queue holds at most 10 drafts; a draft past that is refused. A draft takes `type`, a one-line `title` (at most 120 characters), `details` (at most 8000 characters) and optional `area` and `evidence`; evidence keys left out (tool, endpoint, HTTP status, request id) are filled from the last failing call in this session, and the MCP version and client name are attached to every report.", "execution": { - "name": "reddit_feedback_send" + "name": "reddit_feedback_send", + "title": "Send Feedback Report" }
--- pinned/reddit_home_feed +++ observed/reddit_home_feed { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_home_feed" + "title": "Home Feed" }, "description": "Reads the caller's own Reddit home feed, the front page their subscriptions produce. The other read tools are served from a shared pool of accounts and so cannot return a personal feed; this one sends the caller's session. Requires the caller's Reddit session cookies (session and loid) from POST /api/reddit/login on the REST API, which is not an MCP tool. Without them it returns 400, because Reddit's logged-out front page is a different feed belonging to nobody rather than a thinner version of the caller's. Same post shape and `after` cursor as the subreddit listing. Example: sort='best' limit=25.", "execution": { - "name": "reddit_home_feed" + "name": "reddit_home_feed", + "title": "Home Feed" }
--- pinned/reddit_monitor_add +++ observed/reddit_monitor_add { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": false, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_monitor_add" + "title": "Create Monitor" }, "description": "Creates a Reddit monitor: watches one or more subreddits, or ALL of Reddit, for new posts (or comments, via `kind`) matching a filter, and delivers every match to the account's registered webhooks. With `subreddit` absent and `q` set, it is a sitewide keyword monitor covering every subreddit at once (posts only). By default matches go to EVERY active webhook on the account; `webhook_ids` routes this monitor's matches to specific webhooks. Every redditapis.com account holds a free entitlement of ONE all-of-Reddit post watch at a 60s cadence (up to 10,000 deliveries a day), so that monitor needs no subscription. Naming a `subreddit`, matching comments, a faster cadence and any additional watch require a paid plan. Needs at least one free monitor slot (the `slots` object in the monitor list). Forward-looking only, from the moment of creation or from `baseline_item_id` if given; posts that already existed are not backfilled. Returns the created monitor (with its `id`) on success, or `subscription_required` (402) if the account holds no recognised entitlement at all, `subreddit_scope_requires_paid_plan` (402) if a free account named a `subreddit`, `monitor_slots_exhausted` (402) if the plan's slot limit is reached, `sitewide_slots_exhausted` (402) if the plan's separate sitewide cap is reached, `distinct_subreddit_limit_reached` (402) if the account already watches as many DIFFERENT subreddits as the plan covers (the limit counts distinct subreddits across all the account's monitors, not monitors, and the same subreddit in two monitors counts once; `slots.distinct_subreddits_total` in the monitor list), `sitewide_comment_monitoring_not_available` (501) if a sitewide monitor asks for comments, `subreddit_reserved` (400) if `subreddit` names 'all', `subreddit_not_found` (400) if a named subreddit does not exist, or `webhook_not_found` (400) if a `webhook_ids` entry is not on this account.", "execution": { - "name": "reddit_monitor_add" + "name": "reddit_monitor_add", + "title": "Create Monitor" }
--- pinned/reddit_monitor_deliveries +++ observed/reddit_monitor_deliveries { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_monitor_deliveries" + "title": "List Monitor Deliveries" }, "description": "Delivery history: the actual Reddit posts a monitor's webhook has received (or attempted), newest first, including the real post content (title, subreddit, permalink, author). It records what was sent, not only how many (the counts are in monitor health). Every delivered item also carries `payload.items[].enrichment`: a `relevance.score` (0-1, how much of THIS monitor's own keyword criteria the item matched; not a model's confidence), a `sentiment` (`polarity` -1 to 1 plus a positive/negative/mixed/neutral `label`), and an `intent.tag` (question, recommendation_request, complaint, promotion, praise, or discussion). All three are deterministic keyword, lexicon and rule heuristics computed at no extra cost; each carries its own `method` field and none of them is a machine-learning or LLM call. Without `id`, the history aggregates across every monitor on the account.", "execution": { - "name": "reddit_monitor_deliveries" + "name": "reddit_monitor_deliveries", + "title": "List Monitor Deliveries" }
--- pinned/reddit_monitor_health +++ observed/reddit_monitor_health { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_monitor_health" + "title": "Monitor Health" }, "description": "Per-monitor health: active state, poll cadence, when it last matched something, and delivery counts for the last 24h (`delivered_24h`, `failed_24h`, `suppressed_24h`), plus whether its delivery ceiling has been hit (`ceiling_reached`) and what that ceiling is (`daily_delivery_ceiling`). `suppressed_24h` COUNTS TWO DIFFERENT THINGS AND `suppressed_breakdown` SPLITS THEM: `ceiling` (the monitor's daily cap was reached) and `stale` (the item was already older than the freshness window when first seen, so it was withheld rather than delivered as if it were new). A stale withhold is the usual reason a monitor's delivered count sits far below its matched count with no error anywhere; it is the freshness gate working as intended, not a failure or a plan limit, and retrying cannot recover the items. `ceiling_reached` is the field that indicates the daily cap was hit, and a stale withhold does not set it. When `suppressed_breakdown.resolved` is false the split does not add up to the total and `unresolved_reason` says why; `ceiling_reached` stays conservative in that case rather than being cleared. THOSE COUNTS COVER ONLY POSTS THAT WERE FETCHED. A post that was not fetched was not matched and leaves no row behind, so it is absent from all three counters; zeros there mean no matches were recorded, not that nothing was missed. `coverage_24h` is the separate field that speaks to fetching. Its `status` is `complete` (every poll reached the point where the previous poll finished), `degraded` (at least one poll was truncated and posts were lost unrecoverably; detail in `gaps[]`, `posts_in_window_at_least` and the estimated `posts_missed_estimate`), `partial` (nothing found, but not every kind of loss was checked; detail in `unobserved_events`), or `unknown` (the check could not be run at all; detail in `reason`). `unknown`, `partial` and a null count each mean the question went unanswered. `coverage_24h` is fed by the poll log and covers BOTH ways a poll loses posts unrecoverably (`listing_exhausted`, where Reddit stopped serving older posts, and `poll_overflow`, where the feed outran one poll's page budget); `observed_events` names what was actually checked, and a status of `complete` is awarded only when both were. WHAT `complete` ESTABLISHES: every poll that RAN got back to where the previous poll finished, AND every feed was actually polled. The second half comes from `stream_liveness`. A feed that is not polled writes nothing to the poll log, so on the poll log alone it looks identical to a healthy monitor; `stream_liveness` reads a different source (the polling registry plus the per-feed last-successful-poll stamp), neither of which a poll that did not run can produce. Its `status` is `live` (every feed checked within three times its own interval), `degraded` (at least one is not: `streams[]` marks each feed `never_polled`, `stalled` or `unregistered`), or `unknown` (could not be established, including `streams_awaiting_first_poll` on a monitor created moments ago, which clears itself). `coverage_24h` reads `complete` only while `stream_liveness` is `live`. A zero delivery count on a `never_polled` feed means nothing was checked, not a quiet subreddit. `gaps[]` IS A SAMPLE, NOT THE WHOLE LIST: it carries the most recent gaps only, `gaps_returned` says how many are in it, `gaps_truncated` says whether more exist, and `gap_events_24h` is the true total. `cadence` reports whether the monitor is served the interval its plan sells, which none of the fields above can. `promised_cadence_s` is the floor the account's CURRENT plan includes and `requested_cadence_s` is what this monitor is actually set to; `meets_entitlement` false means the monitor is set slower than the plan allows, which happens because a plan upgrade does not re-cadence monitors that already exist (setting `cadence_s` on the monitor changes it, and a deliberately slower cadence is also a legitimate choice). `last_checked_s_ago`, `freshness_ratio` and `within_margin` describe the slowest feed feeding this monitor, named in `slowest_stream`; `freshness_reading` marks them as ONE INSTANTANEOUS SAMPLE, not an average and not a sustained verdict, so a single reading past the margin is not by itself proof of under-service. These fields are TRI-STATE: a null `within_margin` or `meets_entitlement` means the question could not be answered, and `unknown_reason` says why.", "execution": { - "name": "reddit_monitor_health" + "name": "reddit_monitor_health", + "title": "Monitor Health" }
--- pinned/reddit_monitor_list +++ observed/reddit_monitor_list { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_monitor_list" + "title": "List Monitors" }, "description": "Lists every monitor on the caller's account, with each one's filter, active state, and cadence, plus a `slots` object ({used, total, tier}) showing how many monitor slots are purchased vs in use. Reading the list requires no active plan (a lapsed subscription shows an empty or paused list, not an error). A null `webhook_ids` is the default and means matches go to EVERY active webhook on the account, the normal healthy state, not a monitor without a destination; a non-empty array narrows delivery to those webhook ids. Where a monitor's matches actually went is recorded per delivery, as the resolved `webhook_id` on each delivery row.", "execution": { - "name": "reddit_monitor_list" + "name": "reddit_monitor_list", + "title": "List Monitors" }
--- pinned/reddit_monitor_remove +++ observed/reddit_monitor_remove { "annotations": { "destructiveHint": true, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_monitor_remove" + "title": "Delete Monitor" }, "description": "Deletes a monitor. There is no undo endpoint: it stops matching immediately, its slot is freed for a new monitor, and it disappears from the monitor list. Its past deliveries are NOT erased; they remain queryable in the delivery history (both by monitor id and in the aggregate, no-id view) indefinitely. Returns 404 `monitor_not_found` if the id does not exist or is not on this account.", "execution": { - "name": "reddit_monitor_remove" + "name": "reddit_monitor_remove", + "title": "Delete Monitor" }
--- pinned/reddit_monitor_update +++ observed/reddit_monitor_update { "annotations": { - "destructiveHint": false, - "openWorldHint": true, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_monitor_update" + "title": "Update Monitor" }, "description": "Updates an existing monitor: pauses or resumes it (`active`), changes its poll interval (`cadence_s`), switches between posts and comments (`kind`), replaces its filter entirely, or re-targets which webhooks it delivers to (`webhook_ids`). Any filter field (subreddit, q, kind, domain, etc.) REPLACES the whole filter: it does not merge with the existing one, so a field absent from the request is cleared, and a request without `kind` reverts the monitor to posts-only. `webhook_ids` likewise REPLACES the monitor's targeting outright (an empty array clears back to every active webhook); a request without it leaves the existing targeting untouched. A request with only `active` and/or `cadence_s` changes only those. Returns 404 `monitor_not_found` if the id does not exist or is not on this account, or 400 `webhook_not_found` if a `webhook_ids` entry is not on this account.", "execution": { - "name": "reddit_monitor_update" + "name": "reddit_monitor_update", + "title": "Update Monitor" }
--- pinned/reddit_monitor_webhook_create +++ observed/reddit_monitor_webhook_create { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": false, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_monitor_webhook_create" + "title": "Create Webhook" }, "description": "Registers a delivery target for monitors to send matches to. Requires an active monitoring plan (a webhook with no plan could not receive anything). Returns the webhook with its signing `secret` SHOWN ONCE: the webhook list does not return it again. HTTPS only; the URL is re-validated (including a fresh DNS check) at every delivery, not just at creation.", "execution": { - "name": "reddit_monitor_webhook_create" + "name": "reddit_monitor_webhook_create", + "title": "Create Webhook" }
--- pinned/reddit_monitor_webhook_delete +++ observed/reddit_monitor_webhook_delete { "annotations": { "destructiveHint": true, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": false, - "title": "reddit_monitor_webhook_delete" + "title": "Delete Webhook" }, "description": "Permanently deletes a webhook. A monitor still pointing at it fails to deliver until it is repointed at a different webhook: deletion does NOT cascade-delete or pause the monitors using it. Cannot be undone. Returns 404 `webhook_not_found` if the id does not exist or is not on this account.", "execution": { - "name": "reddit_monitor_webhook_delete" + "name": "reddit_monitor_webhook_delete", + "title": "Delete Webhook" }
--- pinned/reddit_monitor_webhook_list +++ observed/reddit_monitor_webhook_list { "annotations": { "destructiveHint": false, - "openWorldHint": true, + "idempotentHint": true, + "openWorldHint": false, "readOnlyHint": true, - "title": "reddit_monitor_webhook_list" + "title": "List Webhooks" }, "description": "Lists every webhook registered on the caller's account. The signing secret is not included; it is shown once, at creation.", "execution": { - "name": "reddit_monitor_webhook_list" + "name": "reddit_monitor_webhook_list", + "title": "List Webhooks" }
--- pinned/reddit_monitor_webhook_test +++ observed/reddit_monitor_webhook_test { "annotations": { "destructiveHint": false, + "idempotentHint": false, "openWorldHint": true, "readOnlyHint": false, - "title": "reddit_monitor_webhook_test" + "title": "Send Webhook Test" }, "description": "Sends a one-off test delivery to a registered webhook (rate-limited to 10/min), confirming it is wired up correctly before a real match arrives. The test payload is formatted by the webhook's `kind`, the same way a real delivery would be. On failure the response carries `reason` and `status`, plus `hint`, a sentence naming the fix (most often that the target's `kind` does not match its host, which no test can succeed through), and `detail`, a bounded, sanitised copy of what the destination itself replied. `hint` carries the remedy; a bare `reason` such as `http_error` with a 400 names no field, no value and no remedy. Returns 404 `webhook_not_found` if the id does not exist or is not on this account, or `webhook_url_rejected` if the URL fails re-validation (e.g. now resolves to a private address).", "execution": { - "name": "reddit_monitor_webhook_test" + "name": "reddit_monitor_webhook_test", + "title": "Send Webhook Test" }
--- pinned/reddit_post +++ observed/reddit_post { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_post" + "title": "Get Post" }, "description": "Fetches a single Reddit post by its id. Returns the full post object (title, author, score, text, permalink, subreddit, url). Example: id='abc123' (the base-36 id, no t3_ prefix).", "execution": { - "name": "reddit_post" + "name": "reddit_post", + "title": "Get Post" }
--- pinned/reddit_post_comments +++ observed/reddit_post_comments { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_post_comments" + "title": "Get Post Comments" }, "description": "Fetches a single post and its comment tree by permalink. Returns the post plus threaded comments (author, body, score, replies) and an `after` cursor. The `permalink` is the one carried by any post result. Size a big thread with the three knobs: `limit` caps top-level comments (1-500, default 100), `depth` caps reply depth (1-10; `depth=1` is top-level only; absent is the full tree) and `sort` orders them (confidence = Reddit's best, top, new, controversial, old, qa). `limit=20, depth=1, sort='top'` reads the twenty best top-level comments and nothing below them, which keeps a 2,700-comment thread readable. `meta` echoes limit_used, depth_used and sort_used.", "execution": { - "name": "reddit_post_comments" + "name": "reddit_post_comments", + "title": "Get Post Comments" }
--- pinned/reddit_post_visibility +++ observed/reddit_post_visibility { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_post_visibility" + "title": "Post Visibility" }, "description": "Is a post still publicly visible, or did it quietly stop being so? A removed Reddit post still returns when fetched by id, so the post alone does not answer this. This fetches the post and then one page of its author's submitted listing and compares them. Returns a verdict of live, not_visible or undecidable, a plain-language reason, and a confident flag. By design it does not say WHY a post is not visible: a moderator removal, an admin removal, a spam filter and an author who has hidden their history are indistinguishable from outside. undecidable is a real answer, not a failure. When the post resolves but the author listing read fails, the answer is still returned as undecidable with checked.author_listing false and checked.listing_error naming why (upstream_unavailable or upstream_timeout: our read was refused, not billed; listing_unavailable: Reddit answered the listing with 403 or 404, billed). Two upstream calls, billed as one $0.004 dual read.", "execution": { - "name": "reddit_post_visibility" + "name": "reddit_post_visibility", + "title": "Post Visibility" }
--- pinned/reddit_search +++ observed/reddit_search { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_search" + "title": "Search Posts" }, "description": "Searches Reddit posts across all of Reddit or within one subreddit. Returns matching posts with author, score, comments, permalink, and an `after` cursor. `subreddit` scopes the search to one community. Optional advanced filters narrow the results by minimum/maximum score, comment count, media type, and post flags, with an optional re-sort of the page. Filters apply to the returned page, so a filtered response carries a `meta` object with page-completeness counts, and the next page comes from `after`. SORT BEHAVIOUR, measured 2026-09-11: `top`, `new` and `comments` order the MATCHING set by score, date or comment count alone, and Reddit matches loosely (image OCR, comments, and the word 'reddit' is in almost every big post), so a generic multi-word query with sort='top' and no subreddit returns the site-wide viral listing, not the topic. Even a distinctive term is ranked by score alone (q='pgvector' sort='top': 1 of 5 results was about pgvector, the rest were large posts that mention it once). Best match then highest score comes from sort='relevance' with a large `limit`, where sort_type='score' re-orders that returned page (it does not reach past the page); a quoted phrase (q='\"rust vs go\"') or a `subreddit` scope also narrows the matching set. Example: q='rust vs go' sort='relevance' t='year' limit=100 sort_type='score'.", "execution": { - "name": "reddit_search" + "name": "reddit_search", + "title": "Search Posts" }
--- pinned/reddit_search_comments +++ observed/reddit_search_comments { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_search_comments" + "title": "Search Posts by Comment" }, "description": "Searches Reddit by COMMENT text. Reddit's comment search matches the keyword against comment bodies but returns the PARENT POSTS, not the individual comments, so each result is a post whose discussion mentions the query, carrying that post's title, selftext, score, and comment count. It surfaces threads where a topic comes up in the replies, which a post-title search misses. Reddit does not expose which specific comment matched or its text, so the results are posts, not comment bodies. Example: q='best mechanical keyboard' sort='relevance' t='year'.", "execution": { - "name": "reddit_search_comments" + "name": "reddit_search_comments", + "title": "Search Posts by Comment" }
--- pinned/reddit_search_communities +++ observed/reddit_search_communities { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_search_communities" + "title": "Search Communities" }, "description": "Searches for subreddits (communities) by name or topic. Returns matching subreddits with title, subscriber count, description, and NSFW flag. Example: q='machine learning'.", "execution": { - "name": "reddit_search_communities" + "name": "reddit_search_communities", + "title": "Search Communities" }
--- pinned/reddit_search_media +++ observed/reddit_search_media { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_search_media" + "title": "Search Media Posts" }, "description": "Searches Reddit posts filtered to media (images, video, gifs). Returns media posts with the media URL/type, author, score, and the post url. `kind` narrows the results to one media type. Example: q='aurora borealis' kind='image'.", "execution": { - "name": "reddit_search_media" + "name": "reddit_search_media", + "title": "Search Media Posts" }
--- pinned/reddit_search_users +++ observed/reddit_search_users { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_search_users" + "title": "Search Users" }, "description": "Searches for Reddit users (redditors) by name or keyword. Returns matching accounts with username, karma, and account age. Example: q='spez'.", "execution": { - "name": "reddit_search_users" + "name": "reddit_search_users", + "title": "Search Users" }
--- pinned/reddit_set_watch +++ observed/reddit_set_watch { "annotations": { "destructiveHint": false, + "idempotentHint": false, "openWorldHint": true, "readOnlyHint": false, - "title": "reddit_set_watch" + "title": "Create Watch" }, "description": "Creates a Reddit watch from a plain-words description in one call: the description is compiled into a monitor filter, the account's own plan capabilities are read, the monitor is created, and an HTTPS delivery target given in `deliver_to` is registered, pointed at this monitor and test-fired. The compiler reads subreddit names written as r/Name, quoted phrases as keyword terms (two or more become an any-of set), 'comments' or 'posts and comments' as the match kind, terms after 'except' or 'ignoring' as exclusions, and 'at least N upvotes' as a score floor; everything it derived comes back in `understood` and `compiled_filter`. A description naming neither a subreddit nor a keyword is refused before any request is sent, because a monitor is anchored by one or the other. On an account whose plan carries no subreddit-scoped watches, a description that names subreddits and also carries a keyword is built over every subreddit instead, and the swap is reported in `notes`. Monitor and webhook calls are not metered. The result carries the created monitor, the delivery target and its test outcome, and the account's current slot and capability figures.", "execution": { - "name": "reddit_set_watch" + "name": "reddit_set_watch", + "title": "Create Watch" }
--- pinned/reddit_subreddit_about +++ observed/reddit_subreddit_about { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_about" + "title": "Get Subreddit Info" }, "description": "Fetches a subreddit's public metadata by name (Reddit's /r/<name>/about data). Returns the subreddit's title, public description, subscriber count, active-user count, creation timestamp, type, and NSFW flag. Example: name='python'.", "execution": { - "name": "reddit_subreddit_about" + "name": "reddit_subreddit_about", + "title": "Get Subreddit Info" }
--- pinned/reddit_subreddit_comments +++ observed/reddit_subreddit_comments { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_comments" + "title": "List Subreddit Comments" }, "description": "Streams the NEWEST comments across an entire subreddit (Reddit's /r/<name>/comments feed), not one post's thread. Returns comments with body, author, score, subreddit, the parent post link, and timestamp, plus an `after` cursor. Repeated calls surface new comments in a community as they are posted. Example: name='python'.", "execution": { - "name": "reddit_subreddit_comments" + "name": "reddit_subreddit_comments", + "title": "List Subreddit Comments" }
--- pinned/reddit_subreddit_moderators +++ observed/reddit_subreddit_moderators { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_moderators" + "title": "List Subreddit Moderators" }, "description": "Fetches a subreddit's moderator team by name (Reddit's /r/<name>/about/moderators data). Returns a `moderators` list, each with `name`, `id`, `mod_permissions`, `flair_text`, and `added` (when they joined the mod team). Example: name='python'.", "execution": { - "name": "reddit_subreddit_moderators" + "name": "reddit_subreddit_moderators", + "title": "List Subreddit Moderators" }
--- pinned/reddit_subreddit_posts +++ observed/reddit_subreddit_posts { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_posts" + "title": "List Subreddit Posts" }, "description": "Lists posts from a subreddit by sort order: newest, hot, top of a time window, rising, controversial or best. Returns post title, author, score, comment count, and permalink, plus an `after` cursor for paging. When `after` comes back null the response carries `listing_status`: `complete` means no older posts and is only claimed when the whole run came back in under one page, `truncated` means Reddit's cap cut the listing off, and `unknown` means completeness could not be established. A busy feed that Reddit stops serving reports `unknown`, not `complete`. Example: subreddit='programming' sort='top' t='week'.", "execution": { - "name": "reddit_subreddit_posts" + "name": "reddit_subreddit_posts", + "title": "List Subreddit Posts" }
--- pinned/reddit_subreddit_rules +++ observed/reddit_subreddit_rules { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_rules" + "title": "Get Subreddit Rules" }, "description": "Fetches a subreddit's posting rules by name (Reddit's /r/<name>/about/rules data). Returns a `rules` list, each with name, description, what it applies to (posts, comments, or all), violation reason, priority, and creation date, plus a `site_rules` list of Reddit's site-wide rules. Example: name='python'.", "execution": { - "name": "reddit_subreddit_rules" + "name": "reddit_subreddit_rules", + "title": "Get Subreddit Rules" }
--- pinned/reddit_subreddit_top +++ observed/reddit_subreddit_top { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_top" + "title": "Top Subreddit Posts" }, "description": "Gets the TOP posts of a subreddit for a time window, the highest-scoring posts of a community. Returns posts with score, author, comments, and permalink plus an `after` cursor. Example: name='science' t='month'.", "execution": { - "name": "reddit_subreddit_top" + "name": "reddit_subreddit_top", + "title": "Top Subreddit Posts" }
--- pinned/reddit_subreddit_wiki +++ observed/reddit_subreddit_wiki { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddit_wiki" + "title": "Subreddit Wiki Page" }, "description": "Fetches a subreddit's wiki page by name and page (Reddit's /r/<name>/wiki/<page> data). Returns a single object with `content_md` and `content_html`, a `may_revise` flag, and the last revision (`revision_id`, `revision_date`, `revised_by`, `reason`). A wiki commonly holds a community's rules or FAQ. The page may be multi-segment, for example index, rules, or config/sidebar. Example: name='python', page='index'.", "execution": { - "name": "reddit_subreddit_wiki" + "name": "reddit_subreddit_wiki", + "title": "Subreddit Wiki Page" }
--- pinned/reddit_subreddits_default +++ observed/reddit_subreddits_default { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddits_default" + "title": "List Default Subreddits" }, "description": "Browses Reddit's default front-page set of subreddits, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { - "name": "reddit_subreddits_default" + "name": "reddit_subreddits_default", + "title": "List Default Subreddits" }
--- pinned/reddit_subreddits_new +++ observed/reddit_subreddits_new { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddits_new" + "title": "List New Subreddits" }, "description": "Browses the newest subreddits, the communities most recently created, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { - "name": "reddit_subreddits_new" + "name": "reddit_subreddits_new", + "title": "List New Subreddits" }
--- pinned/reddit_subreddits_popular +++ observed/reddit_subreddits_popular { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_subreddits_popular" + "title": "List Popular Subreddits" }, "description": "Browses the most-subscribed, trending subreddits right now, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { - "name": "reddit_subreddits_popular" + "name": "reddit_subreddits_popular", + "title": "List Popular Subreddits" }
--- pinned/reddit_user_achievements +++ observed/reddit_user_achievements { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_achievements" + "title": "Get User Achievements" }, "description": "Lists a Reddit user's public achievements, the trophies shown on their reddit.com/user/<name>/achievements page. Returns each achievement's name, description, granted timestamp and icons, plus a count. An account with none returns an empty list rather than an error, so a zero count is a real answer. Achievements such as One-Year Club and Verified Email reflect account age and standing. Example: name='spez'.", "execution": { - "name": "reddit_user_achievements" + "name": "reddit_user_achievements", + "title": "Get User Achievements" }
--- pinned/reddit_user_comments +++ observed/reddit_user_comments { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_comments" + "title": "List User Comments" }, "description": "Lists a Reddit user's recent comments. Returns comments with body, score, subreddit, parent link, and timestamp plus an `after` cursor. Example: name='spez' sort='top'.", "execution": { - "name": "reddit_user_comments" + "name": "reddit_user_comments", + "title": "List User Comments" }
--- pinned/reddit_user_gilded +++ observed/reddit_user_gilded { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_gilded" + "title": "List Gilded Items" }, "description": "Lists the posts and comments a Reddit account has received an award (gold) on. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { - "name": "reddit_user_gilded" + "name": "reddit_user_gilded", + "title": "List Gilded Items" }
--- pinned/reddit_user_hidden +++ observed/reddit_user_hidden { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_hidden" + "title": "List Hidden Posts" }, "description": "Lists the posts and comments a Reddit account has HIDDEN. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { - "name": "reddit_user_hidden" + "name": "reddit_user_hidden", + "title": "List Hidden Posts" }
--- pinned/reddit_user_profile +++ observed/reddit_user_profile { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_profile" + "title": "Get User Profile" }, "description": "Fetches a Reddit user's public profile by username. Returns account info: username, id, karma (post + comment), account age, verified/employee flags, and avatar. Example: name='spez'.", "execution": { - "name": "reddit_user_profile" + "name": "reddit_user_profile", + "title": "Get User Profile" }
--- pinned/reddit_user_saved +++ observed/reddit_user_saved { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_saved" + "title": "List Saved Items" }, "description": "Lists the posts and comments a Reddit account has SAVED. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { - "name": "reddit_user_saved" + "name": "reddit_user_saved", + "title": "List Saved Items" }
--- pinned/reddit_user_submitted +++ observed/reddit_user_submitted { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_submitted" + "title": "List User Posts" }, "description": "Lists a Reddit user's submitted POSTS (their post history, as distinct from their comments). Returns posts with title, author, score, comment count, and permalink, plus an `after` cursor. Example: name='spez' sort='top'.", "execution": { - "name": "reddit_user_submitted" + "name": "reddit_user_submitted", + "title": "List User Posts" }
--- pinned/reddit_user_upvoted +++ observed/reddit_user_upvoted { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_user_upvoted" + "title": "List Upvoted Posts" }, "description": "Lists the posts and comments a Reddit account has UPVOTED. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { - "name": "reddit_user_upvoted" + "name": "reddit_user_upvoted", + "title": "List Upvoted Posts" }
--- pinned/reddit_verify_comments +++ observed/reddit_verify_comments { "annotations": { "destructiveHint": false, + "idempotentHint": true, "openWorldHint": true, "readOnlyHint": true, - "title": "reddit_verify_comments" + "title": "Verify Comments Still Exist" }, "description": "Checks whether specific Reddit comments still EXIST and are publicly visible, in one batch of up to 100 ids. A READ despite being a POST (ids travel in the body because a hundred of them exceed a URL's length); it costs the same as any other read and changes nothing on Reddit. It distinguishes 'deleted by the author', 'removed by a moderator' and 'still there', which a normal comment fetch cannot. Accepts bare ids and t1_-prefixed fullnames interchangeably. Returns one row per id, in request order, each with a status. Example: ids=['n5abcde','t1_n5fghij'].", "execution": { - "name": "reddit_verify_comments" + "name": "reddit_verify_comments", + "title": "Verify Comments Still Exist" }
Changed 2026-10-15 11:32 UTC
3 descriptions rewritten.
--- pinned/reddit_deep_comment_search +++ observed/reddit_deep_comment_search { "annotations": { "destructiveHint": false, - "description": "Comment search that returns the ACTUAL comments whose body matches the keyword, sorted by score (highest first), with body, score, author, a comment-deep permalink, and the parent post. It fetches each matching post's comment tree and filters the comment bodies, so the results are first-hand opinions and answers rather than parent posts. Premium call (it fans out into several reads): `limit` sets how many parent POSTS to expand, 1-25 (default 5), not how many comments come back. The response's `after` cursor expands the NEXT batch of parent posts. `max_comments` optionally caps how many comments come back (the top-scored are kept). Matching is on the visible comment text at word boundaries (link URLs are ignored), so every result mentions the query where a reader can see it. Best-effort: a deleted or deeply-nested comment may be missed (meta.truncated flags when a tree was too deep). group_by='author' switches to a research mode that returns WHO is talking about the query (distinct people ranked by matching-comment count) in place of a flat comment list, capped by max_authors. Example: q='best mechanical keyboard' sort='relevance' t='year'.", + "description": "Comment search that returns the ACTUAL comments whose body matches the keyword, sorted by score (highest first), with body, score, author, a comment-deep permalink, and the parent post. It fetches each matching post's comment tree and filters the comment bodies, so the results are first-hand opinions and answers rather than parent posts. Premium call (it fans out into several reads): `limit` sets how many parent POSTS to expand, 1-25 (default 5), not how many comments come back. The response's `after` cursor expands the NEXT batch of parent posts. `max_comments` optionally caps how many comments come back (the top-scored are kept). Matching is on the visible comment text at word boundaries (link URLs are ignored), so every result mentions the query where a reader can see it. Best-effort: a deleted or deeply-nested comment may be missed (meta.truncated flags when a tree was too deep). Each parent post is its own comment-tree read and one can fail while the others succeed: meta.partial is true with meta.trees_failed and meta.trees_fetched when that happened, so the result covers fewer parent posts than it scanned; the call fails only when every tree failed and at least one was refused upstream. group_by='author' switches to a research mode that returns WHO is talking about the query (distinct people ranked by matching-comment count) in place of a flat comment list, capped by max_authors. Example: q='best mechanical keyboard' sort='relevance' t='year'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_post_comments +++ observed/reddit_post_comments { "annotations": { "destructiveHint": false, - "description": "Fetches a single post and its comment tree by permalink. Returns the post plus threaded comments (author, body, score, replies) and an `after` cursor. The `permalink` is the one carried by any post result.", + "description": "Fetches a single post and its comment tree by permalink. Returns the post plus threaded comments (author, body, score, replies) and an `after` cursor. The `permalink` is the one carried by any post result. Size a big thread with the three knobs: `limit` caps top-level comments (1-500, default 100), `depth` caps reply depth (1-10; `depth=1` is top-level only; absent is the full tree) and `sort` orders them (confidence = Reddit's best, top, new, controversial, old, qa). `limit=20, depth=1, sort='top'` reads the twenty best top-level comments and nothing below them, which keeps a 2,700-comment thread readable. `meta` echoes limit_used, depth_used and sort_used.", "execution": { "taskSupport": "forbidden" }, + "depth": { + "description": "Maximum reply depth, 1-10. depth=1 returns top-level comments only. Absent, Reddit's full tree is returned.", + "maximum": 10, + "minimum": 1, + "type": "integer" + }, + "limit": { + "description": "Maximum top-level comments, 1-500 (default 100). A small value with depth=1 gives a readable summary of a large thread.", + "maximum": 500, + "minimum": 1, + "type": "integer" + }, "permalink": { "description": "The post permalink path from a prior post result, e.g. '/r/programming/comments/abc123/some_title/'. Required.", "minLength": 1, + }, + "sort": { + "description": "Comment ordering: confidence (Reddit's best), top, new, controversial, old or qa. Absent, Reddit's default for the thread.", + "enum": [ + "confidence", + "top", + "new", + "controversial", + "old", + "qa" + ], + "type": "string" } }, "required": [
--- pinned/reddit_post_visibility +++ observed/reddit_post_visibility { "annotations": { "destructiveHint": false, - "description": "Is a post still publicly visible, or did it quietly stop being so? A removed Reddit post still returns when fetched by id, so the post alone does not answer this. This fetches the post and then one page of its author's submitted listing and compares them. Returns a verdict of live, not_visible or undecidable, a plain-language reason, and a confident flag. By design it does not say WHY a post is not visible: a moderator removal, an admin removal, a spam filter and an author who has hidden their history are indistinguishable from outside. undecidable is a real answer, not a failure. Two upstream calls, billed as one $0.004 dual read.", + "description": "Is a post still publicly visible, or did it quietly stop being so? A removed Reddit post still returns when fetched by id, so the post alone does not answer this. This fetches the post and then one page of its author's submitted listing and compares them. Returns a verdict of live, not_visible or undecidable, a plain-language reason, and a confident flag. By design it does not say WHY a post is not visible: a moderator removal, an admin removal, a spam filter and an author who has hidden their history are indistinguishable from outside. undecidable is a real answer, not a failure. When the post resolves but the author listing read fails, the answer is still returned as undecidable with checked.author_listing false and checked.listing_error naming why (upstream_unavailable or upstream_timeout: our read was refused, not billed; listing_unavailable: Reddit answered the listing with 403 or 404, billed). Two upstream calls, billed as one $0.004 dual read.", "execution": { "taskSupport": "forbidden" },
Changed 2026-10-14 11:59 UTC
2 tools added.
+ tool added: reddit_explain
+ tool added: reddit_set_watch
Changed 2026-10-05 14:35 UTC
36 descriptions rewritten, 8 tools added.
+ tool added: reddit_account_me
--- pinned/reddit_by_id +++ observed/reddit_by_id { "annotations": { "destructiveHint": false, - "description": "Bulk-fetch posts by their t3_ fullnames in ONE call (up to 100), instead of a request per post. Pass a comma-separated list of fullnames you already have from a search or listing to hydrate them. Returns posts with title, author, score, comment count, and permalink, the same post shape as the listing endpoints. The result is NOT always one-to-one with your request, so read `meta`: `listing_status` is `complete` only when every fullname came back, `truncated` is the boolean to branch on, and `missing_fullnames` names exactly which ids did not. Example: fullnames='t3_abc123,t3_def456'.", + "description": "Bulk-fetches posts by their t3_ fullnames in ONE call (up to 100), in place of a request per post. Takes a comma-separated list of fullnames from a search or listing. Returns posts with title, author, score, comment count, and permalink, the same post shape as the listing endpoints. The result is not necessarily one-to-one with the request, and `meta` reports which: `listing_status` is `complete` only when every fullname came back, `truncated` is a boolean, and `missing_fullnames` names exactly which ids did not come back. Example: fullnames='t3_abc123,t3_def456'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_deep_comment_search +++ observed/reddit_deep_comment_search { "annotations": { "destructiveHint": false, - "description": "Genuine comment search: returns the ACTUAL comments whose body matches your keyword, sorted by score (highest first), with body, score, author, a comment-deep permalink, and the parent post. Unlike reddit_search_comments (which returns the parent posts, a Reddit limitation), this fetches each matching post's comment tree and filters the comment bodies for you, so you get first-hand opinions and answers directly. Premium call (it fans out into several reads): `limit` sets how many parent POSTS to expand, 1-25 (default 5), not how many comments come back. To go deeper than one call, paginate: pass the response's `after` cursor back as `after` to expand the NEXT batch of parent posts. `max_comments` optionally caps how many comments come back (the top-scored are kept). Matching is on the visible comment text at word boundaries (link URLs are ignored), so a result always mentions your query where a reader can see it. Best-effort: a deleted or deeply-nested comment may be missed (meta.truncated flags when a tree was too deep). Set group_by='author' for the research mode that returns WHO is talking about your query (distinct people ranked by matching-comment count) instead of a flat comment list, capped by max_authors. Example: q='best mechanical keyboard' sort='top'.", + "description": "Comment search that returns the ACTUAL comments whose body matches the keyword, sorted by score (highest first), with body, score, author, a comment-deep permalink, and the parent post. It fetches each matching post's comment tree and filters the comment bodies, so the results are first-hand opinions and answers rather than parent posts. Premium call (it fans out into several reads): `limit` sets how many parent POSTS to expand, 1-25 (default 5), not how many comments come back. The response's `after` cursor expands the NEXT batch of parent posts. `max_comments` optionally caps how many comments come back (the top-scored are kept). Matching is on the visible comment text at word boundaries (link URLs are ignored), so every result mentions the query where a reader can see it. Best-effort: a deleted or deeply-nested comment may be missed (meta.truncated flags when a tree was too deep). group_by='author' switches to a research mode that returns WHO is talking about the query (distinct people ranked by matching-comment count) in place of a flat comment list, capped by max_authors. Example: q='best mechanical keyboard' sort='relevance' t='year'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "group_by": { - "description": "Set to 'author' for the RESEARCH mode: instead of a flat comment list, return the distinct PEOPLE who mentioned your query, ranked by how many of their comments matched (then total score). Each author has comment_count, total_score, the subreddits they matched in, and their top comment. Omit for the normal comment list.", + "description": "'author' selects the RESEARCH mode: the distinct PEOPLE who mentioned the query, ranked by how many of their comments matched (then total score), in place of a flat comment list. Each author has comment_count, total_score, the subreddits they matched in, and their top comment. Absent, the normal comment list is returned.", "enum": [ "author" ], - "description": "Number of parent POSTS to expand into their comment trees (1-25, default 5). Each is one upstream read, so higher = deeper coverage but slower and more expensive. To go past 25, paginate with `after`.", + "description": "Number of parent POSTS to expand into their comment trees (1-25, default 5). Each is one upstream read, so higher = deeper coverage but slower and more expensive. Beyond 25, the `after` cursor pages to the next batch.", "maximum": 25, "minimum": 1, "type": "integer" - "description": "Only with group_by='author'. Optional cap on how many people are returned (the most prolific first). Omit to return everyone. meta.authors_capped is true when this trimmed the list.", + "description": "Applies only with group_by='author'. Optional cap on how many people are returned (the most prolific first). Absent, every author is returned. meta.authors_capped is true when this trimmed the list.", "maximum": 9007199254740991, "minimum": 1, "type": "integer" - "description": "Optional cap on how many comments are returned; the highest-scored are kept. Omit to return every match. meta.capped is true when this trimmed the result.", + "description": "Optional cap on how many comments are returned; the highest-scored are kept. Absent, every match is returned. meta.capped is true when this trimmed the result.", "maximum": 9007199254740991, "minimum": 1, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false" - "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.", + "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed. Only 'relevance' weights how well a post matches; 'top', 'new' and 'comments' rank every loosely-matching post by that one number, so on a site-wide generic query 'top' returns viral posts that barely mention the terms. A quoted phrase or a `subreddit` scope narrows the matching set itself.", "enum": [ "relevance", "hot", - "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match. Pass 'week' or 'month' to keep results recent and on-topic.", + "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match; 'week' or 'month' keeps results recent and on topic.", "enum": [ "hour", "day",
+ tool added: reddit_feedback_get
+ tool added: reddit_feedback_list
+ tool added: reddit_feedback_send
+ tool added: reddit_home_feed
--- pinned/reddit_monitor_add +++ observed/reddit_monitor_add { "annotations": { "destructiveHint": false, - "description": "Create a new Reddit monitor: watch one or more subreddits, or ALL of Reddit, for new posts (or comments, via `kind`) matching a filter, and get every match delivered to a webhook you've registered with reddit_monitor_webhook_create. Omit `subreddit` and set `q` for a sitewide keyword monitor covering every subreddit at once (posts only). By default matches go to EVERY active webhook on your account; pass `webhook_ids` to route this monitor's matches to only specific webhook(s). Every redditapis.com account holds a free entitlement of ONE all-of-Reddit post watch at a 60s cadence (up to 10,000 deliveries a day), so no subscription is needed to create that monitor. Naming a `subreddit`, matching comments, a faster cadence and any additional watch require a paid plan. Needs at least one monitor slot free (see reddit_monitor_list's `slots`). Forward-looking only from the moment of creation, or from `baseline_item_id` if given -- it never backfills posts that already existed. Returns the created monitor (with its `id`) on success, or `subscription_required` (402) if the account holds no recognised entitlement at all, `subreddit_scope_requires_paid_plan` (402) if a free account named a `subreddit`, `monitor_slots_exhausted` (402) if the plan's slot limit is reached, `sitewide_slots_exhausted` (402) if the plan's separate sitewide cap is reached, `distinct_subreddit_limit_reached` (402) if the account already watches as many DIFFERENT subreddits as the plan covers (the limit counts distinct subreddits across all your monitors, not monitors, and the same subreddit in two monitors counts once; see reddit_monitor_list's `slots.distinct_subreddits_total`), `sitewide_comment_monitoring_not_available` (501) if a sitewide monitor asks for comments, `subreddit_reserved` (400) if `subreddit` names 'all', `subreddit_not_found` (400) if a named subreddit does not exist, or `webhook_not_found` (400) if a `webhook_ids` entry is not yours.", + "description": "Creates a Reddit monitor: watches one or more subreddits, or ALL of Reddit, for new posts (or comments, via `kind`) matching a filter, and delivers every match to the account's registered webhooks. With `subreddit` absent and `q` set, it is a sitewide keyword monitor covering every subreddit at once (posts only). By default matches go to EVERY active webhook on the account; `webhook_ids` routes this monitor's matches to specific webhooks. Every redditapis.com account holds a free entitlement of ONE all-of-Reddit post watch at a 60s cadence (up to 10,000 deliveries a day), so that monitor needs no subscription. Naming a `subreddit`, matching comments, a faster cadence and any additional watch require a paid plan. Needs at least one free monitor slot (the `slots` object in the monitor list). Forward-looking only, from the moment of creation or from `baseline_item_id` if given; posts that already existed are not backfilled. Returns the created monitor (with its `id`) on success, or `subscription_required` (402) if the account holds no recognised entitlement at all, `subreddit_scope_requires_paid_plan` (402) if a free account named a `subreddit`, `monitor_slots_exhausted` (402) if the plan's slot limit is reached, `sitewide_slots_exhausted` (402) if the plan's separate sitewide cap is reached, `distinct_subreddit_limit_reached` (402) if the account already watches as many DIFFERENT subreddits as the plan covers (the limit counts distinct subreddits across all the account's monitors, not monitors, and the same subreddit in two monitors counts once; `slots.distinct_subreddits_total` in the monitor list), `sitewide_comment_monitoring_not_available` (501) if a sitewide monitor asks for comments, `subreddit_reserved` (400) if `subreddit` names 'all', `subreddit_not_found` (400) if a named subreddit does not exist, or `webhook_not_found` (400) if a `webhook_ids` entry is not on this account.", "execution": { "taskSupport": "forbidden" }, - "description": "Only match posts by this Reddit username (without u/).", + "description": "Matches only posts by this Reddit username (without u/).", "maxLength": 200, "type": "string" }, - "description": "A Reddit post fullname (e.g. 't3_abc123') to use as the starting point instead of 'now' -- matching starts strictly after this item. Omit to start from the moment of creation.", + "description": "A Reddit post fullname (e.g. 't3_abc123') used as the starting point in place of 'now': matching starts strictly after this item. Absent, matching starts from the moment of creation.", "type": "string" }, "cadence_s": { - "description": "Requested poll interval in seconds. May only be SLOWER than the plan tier's floor, never faster -- a too-low value is silently clamped up to the tier's minimum. Omit to use the tier's default.", + "description": "Requested poll interval in seconds. A value at or above the plan tier's floor is honoured; a lower value is silently clamped up to the tier's minimum. Absent, the tier's default applies.", "exclusiveMinimum": 0, "maximum": 9007199254740991, "type": "integer" - "description": "Outbound link domains to watch for (e.g. ['example.com']). Matches the post's link URL, any URL inside a self-post/comment body, and a crosspost's original link, including one that only appeared in the original post's body. Exact-or-subdomain match only: 'example.com' matches 'blog.example.com' but never 'notexample.com'. Does NOT resolve shortened links (bit.ly, t.co).", + "description": "Outbound link domains to watch for (e.g. ['example.com']). Matches the post's link URL, any URL inside a self-post/comment body, and a crosspost's original link, including one that only appeared in the original post's body. Exact-or-subdomain match only: 'example.com' matches 'blog.example.com' but not 'notexample.com'. Shortened links (bit.ly, t.co) are not resolved.", "items": { "maxLength": 200, "minLength": 1, - "description": "Subreddits to SUPPRESS, without the r/ prefix (e.g. ['politics', 'AskReddit']). SITEWIDE MONITORS ONLY: pass this only when you have OMITTED `subreddit` and anchored the monitor with `q`. Passing it together with `subreddit` is rejected with a field-level 400 -- a monitor that names its subreddits should drop the unwanted name from that list instead. Up to 50, matched exactly like `subreddit` so 'r/Politics', '/r/politics' and 'politics' are one entry. This is the noise control for an all-of-Reddit keyword watch: it filters DELIVERY only, so it never changes what is polled, never frees quota, and never affects matching in any other subreddit. Independent of exclude_terms -- an item is dropped if either fires.", + "description": "Subreddits to SUPPRESS, without the r/ prefix (e.g. ['politics', 'AskReddit']). SITEWIDE MONITORS ONLY: accepted when `subreddit` is absent and `q` anchors the monitor; sent together with `subreddit` it is rejected with a field-level 400. Up to 50, matched exactly like `subreddit`, so 'r/Politics', '/r/politics' and 'politics' are one entry. This is the noise control for an all-of-Reddit keyword watch. It filters DELIVERY only: it does not change what is polled, free quota, or affect matching in any other subreddit. Independent of exclude_terms; an item is dropped if either fires.", "items": { "minLength": 1, "type": "string" - "description": "Posts containing any of these terms are suppressed even if they otherwise match. Use to cut noise (e.g. exclude 'giveaway' from a brand-mention monitor).", + "description": "Posts containing any of these terms are suppressed even when they otherwise match, e.g. 'giveaway' excluded from a brand-mention monitor.", "items": { "maxLength": 200, "minLength": 1, - "description": "Optional label to bundle multiple matches into one delivery instead of one webhook call per match. Omit for one delivery per matching item.", + "description": "Optional label that bundles multiple matches into one delivery in place of one webhook call per match. Absent, each matching item is its own delivery.", "maxLength": 64, "type": "string" }, - "description": "EVERY one of these terms must appear (AND match) for the post to qualify, on top of any `q`.", + "description": "A post qualifies only when EVERY one of these terms appears (AND match), on top of any `q`.", "items": { "maxLength": 200, "minLength": 1, - "description": "At least ONE of these terms must appear (OR match) for the post to qualify, on top of any `q`.", + "description": "A post qualifies when at least ONE of these terms appears (OR match), on top of any `q`.", "items": { "maxLength": 200, "minLength": 1, - "description": "What to watch in the named subreddits: 'post' (default when omitted), 'comment', or 'both'. Comment monitoring requires a Growth, Pro or Scale plan -- on a lower tier this returns `comment_monitoring_requires_higher_tier` (402). Comments run roughly 7x the volume of posts, so expect proportionally more deliveries and check reddit_monitor_health's delivery ceiling before enabling it on a busy subreddit.", + "description": "What to watch in the named subreddits: 'post' (default when omitted), 'comment', or 'both'. Comment monitoring requires a Growth, Pro or Scale plan; on a lower tier this returns `comment_monitoring_requires_higher_tier` (402). Comments run roughly 7x the volume of posts, so deliveries rise proportionally; the daily delivery ceiling per monitor is reported by monitor health.", "enum": [ "post", "comment", + "min_relevance": { + "description": "AI relevance floor, 0-100. 0 (the default) is off. Above 0, every match is scored by a language model against this monitor's own keywords and anything below the floor is NOT delivered: it is recorded in the delivery history with status 'suppressed' and reason 'low_relevance', carrying its score and a one-line explanation, so what was filtered, and why, stays readable. Nothing is silently discarded. Rough calibration: 80-100 squarely on topic, 50-79 related but peripheral, 20-49 tangential, 0-19 the keyword is used in an unrelated sense. The comparison is inclusive, so a score equal to the floor is delivered. If scoring is unavailable the match is delivered UNSCORED rather than withheld. Requires a Growth, Pro or Scale plan; on a lower tier this returns `ai_relevance_requires_higher_tier` (402). REJECTED with a field-level 400 on a monitor that has no q, include_any or include_all, because there would be no topic to score an item against and the floor could only admit everything.", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, "min_score": { - "description": "Only match posts with at least this many upvotes.", + "description": "Matches only posts with at least this many upvotes.", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Set false to EXCLUDE NSFW/over-18 posts. Omitted or true both mean NSFW is allowed through -- there is no exclude-by-default; you must explicitly pass false to filter it out. POSTS ONLY: nsfw=false is REJECTED with a field-level 400 when kind is 'comment' or 'both', because Reddit flags NSFW on a post and never on an individual comment, so there is no field to filter a comment on. Run a kind='post' monitor to keep NSFW filtering, and cut unwanted comment text with exclude_terms.", + "description": "false EXCLUDES NSFW/over-18 posts. Omitted or true both let NSFW through; there is no exclude-by-default, and only an explicit false filters it. POSTS ONLY: nsfw=false is REJECTED with a field-level 400 when kind is 'comment' or 'both', because Reddit flags NSFW on a post and not on an individual comment, so there is no field to filter a comment on. NSFW filtering is available on kind='post' monitors, and exclude_terms filters comment text.", "type": "boolean" }, "q": { - "description": "Free-text keyword or phrase to match. Matched against the fields in `search_in` (default title+body+url). Omit to match every new post in the watched subreddits.", + "description": "Free-text keyword or phrase to match, against the fields in `search_in` (default title+body+url). Absent, every new post in the watched subreddits matches.", "maxLength": 200, "type": "string" }, - "description": "Which fields keyword/term matching is scoped to. Default ['title', 'body', 'url']. Narrow to avoid false positives, e.g. a term that only appears in a URL slug matching a post that never mentions it in prose. All four resolve on comments as well as posts: on a comment, 'title' matches the title of the THREAD the comment sits under (a comment has no title of its own), which also applies through the default scope and can deliver every comment under a busy matching thread. Scope to ['body'] if you only want comments that say the term themselves.", + "description": "Which fields keyword/term matching is scoped to. Default ['title', 'body', 'url']. A narrower scope avoids false positives, e.g. a term that only appears in a URL slug matching a post that does not mention it in prose. All four resolve on comments as well as posts: on a comment, 'title' matches the title of the THREAD the comment sits under (a comment has no title of its own), which also applies through the default scope and can deliver every comment under a busy matching thread. ['body'] limits comment matches to comments that contain the term themselves.", "items": { "enum": [ "title", - "description": "Subreddits to watch, without the r/ prefix (e.g. ['SaaS', 'startups']). 1 to 50. OMIT this entirely (and set `q`) to watch ALL of Reddit for a keyword -- a monitor must be anchored by either a subreddit list or a keyword, never neither. Do NOT pass ['all']: r/all is Reddit's site-wide listing rather than a subreddit, so it is refused with 400 subreddit_reserved. Sitewide monitors are capped per plan tier (see reddit_monitor_list's `slots`) and cover POSTS only.", + "description": "Subreddits to watch, without the r/ prefix (e.g. ['SaaS', 'startups']). 1 to 50. Absent, with `q` set, the monitor watches ALL of Reddit for that keyword. A monitor is anchored by a subreddit list, a keyword or both; a request with neither is refused. ['all'] is refused with 400 subreddit_reserved, because r/all is Reddit's site-wide listing rather than a subreddit. Sitewide monitors are capped per plan tier (the `slots` object in the monitor list) and cover POSTS only.", "items": { "minLength": 1, "type": "string" - "description": "Restrict delivery to specific webhook(s) instead of every active webhook on the account. Pass id(s) from reddit_monitor_webhook_create/reddit_monitor_webhook_list. Omit (or pass an empty array) for the default: deliver to every active webhook you've registered. Every id must be a webhook you own -- returns `webhook_not_found` (400) otherwise.", + "description": "Webhook ids that restrict this monitor's delivery to those webhooks. Omitted or an empty array means the default: delivery to every active webhook on the account. An id that is not a webhook on this account returns `webhook_not_found` (400).", "items": { "minLength": 1, "type": "string"
--- pinned/reddit_monitor_deliveries +++ observed/reddit_monitor_deliveries { "annotations": { "destructiveHint": false, - "description": "Delivery history: the actual Reddit posts a monitor's webhook has received (or attempted), newest first, including the real post content (title, subreddit, permalink, author). Answers 'what did I actually get sent', not just 'how many' (see reddit_monitor_health for counts). Omit `id` to aggregate history across every monitor you own.", + "description": "Delivery history: the actual Reddit posts a monitor's webhook has received (or attempted), newest first, including the real post content (title, subreddit, permalink, author). It records what was sent, not only how many (the counts are in monitor health). Every delivered item also carries `payload.items[].enrichment`: a `relevance.score` (0-1, how much of THIS monitor's own keyword criteria the item matched; not a model's confidence), a `sentiment` (`polarity` -1 to 1 plus a positive/negative/mixed/neutral `label`), and an `intent.tag` (question, recommendation_request, complaint, promotion, praise, or discussion). All three are deterministic keyword, lexicon and rule heuristics computed at no extra cost; each carries its own `method` field and none of them is a machine-learning or LLM call. Without `id`, the history aggregates across every monitor on the account.", "execution": { "taskSupport": "forbidden" }, - "description": "ISO 8601 timestamp cursor for pagination -- pass the `created_at` of the oldest row from the previous page to fetch older deliveries.", + "description": "ISO 8601 timestamp cursor for pagination: the `created_at` of the oldest row from the previous page returns older deliveries.", "type": "string" }, "id": { - "description": "Narrow to one monitor's history. Omit to aggregate across every monitor you own.", + "description": "Narrows to one monitor's history. Absent, the history aggregates across every monitor on the account.", "type": "string" }, "limit": { - "description": "Max rows to return, 1 to 200. Default 50.", + "description": "Maximum rows returned, 1 to 200. Default 50.", "maximum": 200, "minimum": 1, "type": "integer" - "description": "Filter to one delivery status. 'dead' = retries exhausted, gave up. 'suppressed' = matched but deliberately not sent, and `payload.suppressed.reason` says which of the two reasons applied: 'delivery_ceiling' (the monitor's daily cap) or 'stale_item' (the item was already older than the freshness window when we first saw it, so it was withheld rather than delivered as if it were new). A 'stale_item' row is NOT a fault and was NOT rejected by any plan limit, and retrying cannot recover it; `payload.suppressed` carries the age and the threshold. Omit for all statuses.", + "description": "Filters to one delivery status. 'dead' = retries exhausted, gave up. 'suppressed' = matched but deliberately not sent, and `payload.suppressed.reason` says which of the two reasons applied: 'delivery_ceiling' (the monitor's daily cap) or 'stale_item' (the item was already older than the freshness window when first seen, so it was withheld rather than delivered as if it were new). A 'stale_item' row is NOT a fault and was NOT rejected by any plan limit, and retrying cannot recover it; `payload.suppressed` carries the age and the threshold. Absent, every status is returned.", "enum": [ "pending", "delivered",
--- pinned/reddit_monitor_health +++ observed/reddit_monitor_health { "annotations": { "destructiveHint": false, - "description": "Per-monitor health: whether it's active, its poll cadence, when it last matched something, and delivery counts for the last 24h (`delivered_24h`, `failed_24h`, `suppressed_24h`) plus whether its delivery ceiling has been hit (`ceiling_reached`) and what that ceiling is (`daily_delivery_ceiling`). `suppressed_24h` COUNTS TWO DIFFERENT THINGS AND `suppressed_breakdown` SPLITS THEM: `ceiling` (the monitor's daily cap was reached) and `stale` (the item was already older than the freshness window when we first saw it, so it was withheld rather than delivered as if it were new). A stale withhold is the usual reason a monitor's delivered count sits far below its matched count with no error anywhere, and it is the freshness gate working as intended -- nothing failed, the endpoint is fine, no limit on the plan rejected them, and retrying cannot recover the items. Do NOT read `suppressed_24h > 0` as 'this monitor is over its limit': `ceiling_reached` is the field that answers that, and a stale withhold deliberately does not set it. When `suppressed_breakdown.resolved` is false the counts do not add up to the total and `unresolved_reason` says why, so do not report a split off them; `ceiling_reached` stays conservative in that case rather than being cleared. THOSE COUNTS ONLY COVER POSTS WE FETCHED. A post we never fetched was never matched and leaves no row behind, so it is absent from all three counters rather than counted in any of them -- zeros there mean 'no matches recorded', never 'nothing was missed'. `coverage_24h` is the separate field that speaks to fetching, and its `status` is the one to read first: `complete` (every poll reached the point where the previous poll finished), `degraded` (at least one poll was truncated and posts were lost unrecoverably -- see `gaps[]`, `posts_in_window_at_least` and the estimated `posts_missed_estimate`), `partial` (nothing found, but not every kind of loss was checked -- see `unobserved_events`), or `unknown` (the check could not be run at all -- see `reason`). Do NOT report a monitor as healthy on `unknown` or `partial`, and do not treat a null count as zero: both mean the question went unanswered. `coverage_24h` is fed by the poll log and covers BOTH ways a poll loses posts unrecoverably (`listing_exhausted`, where Reddit stopped serving older posts, and `poll_overflow`, where the feed outran one poll's page budget); `observed_events` names what was actually checked, and a status of `complete` is only ever awarded when both were. WHAT `complete` DOES AND DOES NOT ESTABLISH: it means every poll that RAN got back to where the previous poll finished, AND that every feed was actually polled. The second half is `stream_liveness`, the field to read FIRST. `coverage_24h` is computed from the poll log, and a feed that is never polled writes nothing to that log, so it records no truncated poll and the coverage read comes back clean -- identical to a healthy monitor. `stream_liveness` reads a different source (the polling registry plus the per-feed last-successful-poll stamp), neither of which a poll that never ran can produce. Its `status` is `live` (every feed checked within three times its own interval), `degraded` (at least one is not: see `streams[]` for `never_polled`, `stalled` or `unregistered` per feed), or `unknown` (could not be established, including `streams_awaiting_first_poll` on a monitor created moments ago, which clears itself). `coverage_24h` can never read `complete` while this is anything but `live`. Do NOT report a monitor as healthy unless `stream_liveness.status` is `live`, and do not read a zero delivery count on a `never_polled` feed as a quiet subreddit: nothing was checked, so nothing could match. `gaps[]` IS A SAMPLE, NOT THE WHOLE LIST: it carries the most recent gaps only, `gaps_returned` says how many are in it, `gaps_truncated` says whether more exist, and `gap_events_24h` is always the true total. Count gaps from `gap_events_24h` and never from the length of `gaps[]`. FINALLY, `cadence` ANSWERS 'AM I BEING CHECKED AS OFTEN AS I PAY TO BE', which none of the fields above can. Cadence is a priced feature, so this is the field that says whether the monitor is being served the interval its plan sells. `promised_cadence_s` is the floor the account's CURRENT plan includes and `requested_cadence_s` is what this monitor is actually set to; `meets_entitlement` false means the monitor is set slower than the plan allows, which happens because a plan upgrade does NOT re-cadence monitors that already exist -- the fix is to set `cadence_s` on the monitor, and a deliberately slower cadence is also a legitimate choice. `last_checked_s_ago`, `freshness_ratio` and `within_margin` describe the slowest feed feeding this monitor, named in `slowest_stream`. READ `freshness_reading` BEFORE QUOTING ANY OF THEM: it is ONE INSTANTANEOUS SAMPLE, not an average and not a sustained verdict, so a single reading past the margin is not by itself proof of under-service. Every one of these is TRI-STATE and null NEVER means fine: a null `within_margin` or `meets_entitlement` means the question could not be answered, and `unknown_reason` says why. Do not report a monitor as on-cadence on a null.", + "description": "Per-monitor health: active state, poll cadence, when it last matched something, and delivery counts for the last 24h (`delivered_24h`, `failed_24h`, `suppressed_24h`), plus whether its delivery ceiling has been hit (`ceiling_reached`) and what that ceiling is (`daily_delivery_ceiling`). `suppressed_24h` COUNTS TWO DIFFERENT THINGS AND `suppressed_breakdown` SPLITS THEM: `ceiling` (the monitor's daily cap was reached) and `stale` (the item was already older than the freshness window when first seen, so it was withheld rather than delivered as if it were new). A stale withhold is the usual reason a monitor's delivered count sits far below its matched count with no error anywhere; it is the freshness gate working as intended, not a failure or a plan limit, and retrying cannot recover the items. `ceiling_reached` is the field that indicates the daily cap was hit, and a stale withhold does not set it. When `suppressed_breakdown.resolved` is false the split does not add up to the total and `unresolved_reason` says why; `ceiling_reached` stays conservative in that case rather than being cleared. THOSE COUNTS COVER ONLY POSTS THAT WERE FETCHED. A post that was not fetched was not matched and leaves no row behind, so it is absent from all three counters; zeros there mean no matches were recorded, not that nothing was missed. `coverage_24h` is the separate field that speaks to fetching. Its `status` is `complete` (every poll reached the point where the previous poll finished), `degraded` (at least one poll was truncated and posts were lost unrecoverably; detail in `gaps[]`, `posts_in_window_at_least` and the estimated `posts_missed_estimate`), `partial` (nothing found, but not every kind of loss was checked; detail in `unobserved_events`), or `unknown` (the check could not be run at all; detail in `reason`). `unknown`, `partial` and a null count each mean the question went unanswered. `coverage_24h` is fed by the poll log and covers BOTH ways a poll loses posts unrecoverably (`listing_exhausted`, where Reddit stopped serving older posts, and `poll_overflow`, where the feed outran one poll's page budget); `observed_events` names what was actually checked, and a status of `complete` is awarded only when both were. WHAT `complete` ESTABLISHES: every poll that RAN got back to where the previous poll finished, AND every feed was actually polled. The second half comes from `stream_liveness`. A feed that is not polled writes nothing to the poll log, so on the poll log alone it looks identical to a healthy monitor; `stream_liveness` reads a different source (the polling registry plus the per-feed last-successful-poll stamp), neither of which a poll that did not run can produce. Its `status` is `live` (every feed checked within three times its own interval), `degraded` (at least one is not: `streams[]` marks each feed `never_polled`, `stalled` or `unregistered`), or `unknown` (could not be established, including `streams_awaiting_first_poll` on a monitor created moments ago, which clears itself). `coverage_24h` reads `complete` only while `stream_liveness` is `live`. A zero delivery count on a `never_polled` feed means nothing was checked, not a quiet subreddit. `gaps[]` IS A SAMPLE, NOT THE WHOLE LIST: it carries the most recent gaps only, `gaps_returned` says how many are in it, `gaps_truncated` says whether more exist, and `gap_events_24h` is the true total. `cadence` reports whether the monitor is served the interval its plan sells, which none of the fields above can. `promised_cadence_s` is the floor the account's CURRENT plan includes and `requested_cadence_s` is what this monitor is actually set to; `meets_entitlement` false means the monitor is set slower than the plan allows, which happens because a plan upgrade does not re-cadence monitors that already exist (setting `cadence_s` on the monitor changes it, and a deliberately slower cadence is also a legitimate choice). `last_checked_s_ago`, `freshness_ratio` and `within_margin` describe the slowest feed feeding this monitor, named in `slowest_stream`; `freshness_reading` marks them as ONE INSTANTANEOUS SAMPLE, not an average and not a sustained verdict, so a single reading past the margin is not by itself proof of under-service. These fields are TRI-STATE: a null `within_margin` or `meets_entitlement` means the question could not be answered, and `unknown_reason` says why.", "execution": { "taskSupport": "forbidden" }, - "description": "The monitor's id, from reddit_monitor_add's response or reddit_monitor_list.", + "description": "The monitor's id, as returned when the monitor was created or listed.", "minLength": 1, "type": "string" }
--- pinned/reddit_monitor_list +++ observed/reddit_monitor_list { "annotations": { "destructiveHint": false, - "description": "List every monitor on the caller's account, with each one's filter, active state, and cadence, plus a `slots` object ({used, total, tier}) showing how many monitor slots are purchased vs in use. Reading your own list never requires an active plan (a lapsed subscription shows an empty or paused list, not an error). `webhook_ids` NULL DOES NOT MEAN THE MONITOR HAS NO DESTINATION: null is the default and means matches go to EVERY active webhook on the account, which is the normal healthy state. A non-empty array narrows delivery to just those webhook ids. Never report a monitor as having no delivery target on the strength of a null here -- to see where a monitor's matches actually went, read reddit_monitor_deliveries, whose rows carry the resolved `webhook_id`.", + "description": "Lists every monitor on the caller's account, with each one's filter, active state, and cadence, plus a `slots` object ({used, total, tier}) showing how many monitor slots are purchased vs in use. Reading the list requires no active plan (a lapsed subscription shows an empty or paused list, not an error). A null `webhook_ids` is the default and means matches go to EVERY active webhook on the account, the normal healthy state, not a monitor without a destination; a non-empty array narrows delivery to those webhook ids. Where a monitor's matches actually went is recorded per delivery, as the resolved `webhook_id` on each delivery row.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_monitor_remove +++ observed/reddit_monitor_remove { "annotations": { "destructiveHint": true, - "description": "Delete a monitor. There is no undo endpoint -- it stops matching immediately, its slot is freed for a new monitor, and it disappears from reddit_monitor_list. Its past deliveries are NOT erased: they remain queryable via reddit_monitor_deliveries (both scoped by monitor_id and in the aggregate, no-id view) forever. Returns 404 `monitor_not_found` if the id does not exist or is not yours.", + "description": "Deletes a monitor. There is no undo endpoint: it stops matching immediately, its slot is freed for a new monitor, and it disappears from the monitor list. Its past deliveries are NOT erased; they remain queryable in the delivery history (both by monitor id and in the aggregate, no-id view) indefinitely. Returns 404 `monitor_not_found` if the id does not exist or is not on this account.", "execution": { "taskSupport": "forbidden" }, - "description": "The monitor's id, from reddit_monitor_add's response or reddit_monitor_list.", + "description": "The monitor's id, as returned when the monitor was created or listed.", "minLength": 1, "type": "string" }
--- pinned/reddit_monitor_update +++ observed/reddit_monitor_update { "annotations": { "destructiveHint": false, - "description": "Update an existing monitor: pause/resume it (`active`), change its poll interval (`cadence_s`), switch between posts and comments (`kind`), replace its filter entirely, or re-target which webhook(s) it delivers to (`webhook_ids`). IMPORTANT: if you pass ANY filter field (subreddit, q, kind, domain, etc.), it REPLACES the whole filter, it does not merge with the existing one -- resupply every field you want kept, including `subreddit` AND `kind` (omitting `kind` reverts that monitor to posts-only). Same rule for `webhook_ids`: passing it REPLACES the monitor's targeting outright (an empty array clears back to 'every active webhook'); omitting it entirely leaves the monitor's existing targeting untouched. Omit all filter/webhook_ids fields to change only `active`/`cadence_s`. Returns 404 `monitor_not_found` if the id does not exist or is not yours, or 400 `webhook_not_found` if a `webhook_ids` entry is not yours.", + "description": "Updates an existing monitor: pauses or resumes it (`active`), changes its poll interval (`cadence_s`), switches between posts and comments (`kind`), replaces its filter entirely, or re-targets which webhooks it delivers to (`webhook_ids`). Any filter field (subreddit, q, kind, domain, etc.) REPLACES the whole filter: it does not merge with the existing one, so a field absent from the request is cleared, and a request without `kind` reverts the monitor to posts-only. `webhook_ids` likewise REPLACES the monitor's targeting outright (an empty array clears back to every active webhook); a request without it leaves the existing targeting untouched. A request with only `active` and/or `cadence_s` changes only those. Returns 404 `monitor_not_found` if the id does not exist or is not on this account, or 400 `webhook_not_found` if a `webhook_ids` entry is not on this account.", "execution": { "taskSupport": "forbidden" }, - "description": "Set false to pause the monitor (stops matching/delivering), true to resume it.", + "description": "false pauses the monitor (stops matching and delivering); true resumes it.", "type": "boolean" }, "author": { - "description": "Only match posts by this Reddit username (without u/).", + "description": "Matches only posts by this Reddit username (without u/).", "maxLength": 200, "type": "string" }, - "description": "New poll interval in seconds, same tier-floor clamping as reddit_monitor_add.", + "description": "New poll interval in seconds, clamped to the tier floor the same way as at creation.", "exclusiveMinimum": 0, "maximum": 9007199254740991, "type": "integer" - "description": "Outbound link domains to watch for (e.g. ['example.com']). Matches the post's link URL, any URL inside a self-post/comment body, and a crosspost's original link, including one that only appeared in the original post's body. Exact-or-subdomain match only: 'example.com' matches 'blog.example.com' but never 'notexample.com'. Does NOT resolve shortened links (bit.ly, t.co).", + "description": "Outbound link domains to watch for (e.g. ['example.com']). Matches the post's link URL, any URL inside a self-post/comment body, and a crosspost's original link, including one that only appeared in the original post's body. Exact-or-subdomain match only: 'example.com' matches 'blog.example.com' but not 'notexample.com'. Shortened links (bit.ly, t.co) are not resolved.", "items": { "maxLength": 200, "minLength": 1, - "description": "Subreddits to SUPPRESS, without the r/ prefix (e.g. ['politics', 'AskReddit']). SITEWIDE MONITORS ONLY: pass this only when you have OMITTED `subreddit` and anchored the monitor with `q`. Passing it together with `subreddit` is rejected with a field-level 400 -- a monitor that names its subreddits should drop the unwanted name from that list instead. Up to 50, matched exactly like `subreddit` so 'r/Politics', '/r/politics' and 'politics' are one entry. This is the noise control for an all-of-Reddit keyword watch: it filters DELIVERY only, so it never changes what is polled, never frees quota, and never affects matching in any other subreddit. Independent of exclude_terms -- an item is dropped if either fires.", + "description": "Subreddits to SUPPRESS, without the r/ prefix (e.g. ['politics', 'AskReddit']). SITEWIDE MONITORS ONLY: accepted when `subreddit` is absent and `q` anchors the monitor; sent together with `subreddit` it is rejected with a field-level 400. Up to 50, matched exactly like `subreddit`, so 'r/Politics', '/r/politics' and 'politics' are one entry. This is the noise control for an all-of-Reddit keyword watch. It filters DELIVERY only: it does not change what is polled, free quota, or affect matching in any other subreddit. Independent of exclude_terms; an item is dropped if either fires.", "items": { "minLength": 1, "type": "string" - "description": "Posts containing any of these terms are suppressed even if they otherwise match. Use to cut noise (e.g. exclude 'giveaway' from a brand-mention monitor).", + "description": "Posts containing any of these terms are suppressed even when they otherwise match, e.g. 'giveaway' excluded from a brand-mention monitor.", "items": { "maxLength": 200, "minLength": 1, - "description": "Optional label to bundle multiple matches into one delivery instead of one webhook call per match. Omit for one delivery per matching item.", + "description": "Optional label that bundles multiple matches into one delivery in place of one webhook call per match. Absent, each matching item is its own delivery.", "maxLength": 64, "type": "string" }, - "description": "The monitor's id, from reddit_monitor_add's response or reddit_monitor_list.", + "description": "The monitor's id, as returned when the monitor was created or listed.", "minLength": 1, "type": "string" }, - "description": "EVERY one of these terms must appear (AND match) for the post to qualify, on top of any `q`.", + "description": "A post qualifies only when EVERY one of these terms appears (AND match), on top of any `q`.", "items": { "maxLength": 200, "minLength": 1, - "description": "At least ONE of these terms must appear (OR match) for the post to qualify, on top of any `q`.", + "description": "A post qualifies when at least ONE of these terms appears (OR match), on top of any `q`.", "items": { "maxLength": 200, "minLength": 1, - "description": "What to watch in the named subreddits: 'post' (default when omitted), 'comment', or 'both'. Comment monitoring requires a Growth, Pro or Scale plan -- on a lower tier this returns `comment_monitoring_requires_higher_tier` (402). Comments run roughly 7x the volume of posts, so expect proportionally more deliveries and check reddit_monitor_health's delivery ceiling before enabling it on a busy subreddit.", + "description": "What to watch in the named subreddits: 'post' (default when omitted), 'comment', or 'both'. Comment monitoring requires a Growth, Pro or Scale plan; on a lower tier this returns `comment_monitoring_requires_higher_tier` (402). Comments run roughly 7x the volume of posts, so deliveries rise proportionally; the daily delivery ceiling per monitor is reported by monitor health.", "enum": [ "post", "comment", + "min_relevance": { + "description": "AI relevance floor, 0-100. 0 (the default) is off. Above 0, every match is scored by a language model against this monitor's own keywords and anything below the floor is NOT delivered: it is recorded in the delivery history with status 'suppressed' and reason 'low_relevance', carrying its score and a one-line explanation, so what was filtered, and why, stays readable. Nothing is silently discarded. Rough calibration: 80-100 squarely on topic, 50-79 related but peripheral, 20-49 tangential, 0-19 the keyword is used in an unrelated sense. The comparison is inclusive, so a score equal to the floor is delivered. If scoring is unavailable the match is delivered UNSCORED rather than withheld. Requires a Growth, Pro or Scale plan; on a lower tier this returns `ai_relevance_requires_higher_tier` (402). REJECTED with a field-level 400 on a monitor that has no q, include_any or include_all, because there would be no topic to score an item against and the floor could only admit everything.", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, "min_score": { - "description": "Only match posts with at least this many upvotes.", + "description": "Matches only posts with at least this many upvotes.", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Set false to EXCLUDE NSFW/over-18 posts. Omitted or true both mean NSFW is allowed through -- there is no exclude-by-default; you must explicitly pass false to filter it out. POSTS ONLY: nsfw=false is REJECTED with a field-level 400 when kind is 'comment' or 'both', because Reddit flags NSFW on a post and never on an individual comment, so there is no field to filter a comment on. Run a kind='post' monitor to keep NSFW filtering, and cut unwanted comment text with exclude_terms.", + "description": "false EXCLUDES NSFW/over-18 posts. Omitted or true both let NSFW through; there is no exclude-by-default, and only an explicit false filters it. POSTS ONLY: nsfw=false is REJECTED with a field-level 400 when kind is 'comment' or 'both', because Reddit flags NSFW on a post and not on an individual comment, so there is no field to filter a comment on. NSFW filtering is available on kind='post' monitors, and exclude_terms filters comment text.", "type": "boolean" }, "q": { - "description": "Free-text keyword or phrase to match. Matched against the fields in `search_in` (default title+body+url). Omit to match every new post in the watched subreddits.", + "description": "Free-text keyword or phrase to match, against the fields in `search_in` (default title+body+url). Absent, every new post in the watched subreddits matches.", "maxLength": 200, "type": "string" }, - "description": "Which fields keyword/term matching is scoped to. Default ['title', 'body', 'url']. Narrow to avoid false positives, e.g. a term that only appears in a URL slug matching a post that never mentions it in prose. All four resolve on comments as well as posts: on a comment, 'title' matches the title of the THREAD the comment sits under (a comment has no title of its own), which also applies through the default scope and can deliver every comment under a busy matching thread. Scope to ['body'] if you only want comments that say the term themselves.", + "description": "Which fields keyword/term matching is scoped to. Default ['title', 'body', 'url']. A narrower scope avoids false positives, e.g. a term that only appears in a URL slug matching a post that does not mention it in prose. All four resolve on comments as well as posts: on a comment, 'title' matches the title of the THREAD the comment sits under (a comment has no title of its own), which also applies through the default scope and can deliver every comment under a busy matching thread. ['body'] limits comment matches to comments that contain the term themselves.", "items": { "enum": [ "title", - "description": "Subreddits to watch, without the r/ prefix (e.g. ['SaaS', 'startups']). 1 to 50. OMIT this entirely (and set `q`) to watch ALL of Reddit for a keyword -- a monitor must be anchored by either a subreddit list or a keyword, never neither. Do NOT pass ['all']: r/all is Reddit's site-wide listing rather than a subreddit, so it is refused with 400 subreddit_reserved. Sitewide monitors are capped per plan tier (see reddit_monitor_list's `slots`) and cover POSTS only. On an update, omitting this while passing another filter field makes the monitor SITEWIDE (the filter is replaced wholesale, not merged), so resupply it if you meant to keep the monitor scoped.", + "description": "Subreddits to watch, without the r/ prefix (e.g. ['SaaS', 'startups']). 1 to 50. Absent, with `q` set, the monitor watches ALL of Reddit for that keyword. A monitor is anchored by a subreddit list, a keyword or both; a request with neither is refused. ['all'] is refused with 400 subreddit_reserved, because r/all is Reddit's site-wide listing rather than a subreddit. Sitewide monitors are capped per plan tier (the `slots` object in the monitor list) and cover POSTS only. On an update, a request that carries another filter field without this one makes the monitor SITEWIDE (the filter is replaced wholesale, not merged).", "items": { "minLength": 1, "type": "string" - "description": "Restrict delivery to specific webhook(s) instead of every active webhook on the account. Pass id(s) from reddit_monitor_webhook_create/reddit_monitor_webhook_list. Omit (or pass an empty array) for the default: deliver to every active webhook you've registered. Every id must be a webhook you own -- returns `webhook_not_found` (400) otherwise.", + "description": "Webhook ids that restrict this monitor's delivery to those webhooks. Omitted or an empty array means the default: delivery to every active webhook on the account. An id that is not a webhook on this account returns `webhook_not_found` (400).", "items": { "minLength": 1, "type": "string"
--- pinned/reddit_monitor_webhook_create +++ observed/reddit_monitor_webhook_create { "annotations": { "destructiveHint": false, - "description": "Register a delivery target for monitors to send matches to. Requires an active monitoring plan (a webhook with no plan could never receive anything). Returns the webhook with its signing `secret` SHOWN ONCE -- store it immediately, it is never returned again by reddit_monitor_webhook_list. HTTPS only; the URL is re-validated (including a fresh DNS check) at every delivery, not just at creation.", + "description": "Registers a delivery target for monitors to send matches to. Requires an active monitoring plan (a webhook with no plan could not receive anything). Returns the webhook with its signing `secret` SHOWN ONCE: the webhook list does not return it again. HTTPS only; the URL is re-validated (including a fresh DNS check) at every delivery, not just at creation.", "execution": { "taskSupport": "forbidden" }, - "description": "Payload shape to send. PREFER OMITTING THIS: for a hooks.slack.com or discord.com/api/webhooks URL the kind is inferred from the host, and the response reports what it inferred in `kind_inferred_from`. 'slack'/'discord' format as native incoming-webhook messages; 'webhook' sends redditapis' generic signed JSON envelope and is the fallback only for a host we do not recognise; 'email' is not yet a real delivery transport. Passing 'webhook' for a Slack or Discord URL does NOT force the generic envelope (that combination can never deliver -- Slack answers 400 invalid_payload); the host wins and `kind_corrected_from` says so. Passing one SPECIFIC kind for a different platform's host (e.g. 'discord' with a hooks.slack.com URL) is refused with `webhook_kind_mismatch` (400) rather than stored.", + "description": "Payload shape. Absent, the kind is inferred from the host for a hooks.slack.com or discord.com/api/webhooks URL, and the response reports the inference in `kind_inferred_from`. 'slack'/'discord' format as native incoming-webhook messages; 'webhook' sends the generic signed JSON envelope and is the fallback for an unrecognised host; 'email' is not yet a real delivery transport. 'webhook' with a Slack or Discord URL does NOT force the generic envelope (that combination cannot deliver: Slack answers 400 invalid_payload); the host wins and `kind_corrected_from` says so. A SPECIFIC kind for a different platform's host (e.g. 'discord' with a hooks.slack.com URL) is refused with `webhook_kind_mismatch` (400) rather than stored.", "enum": [ "webhook", "slack", - "description": "HTTPS URL to deliver matches to. Must be publicly reachable HTTPS, no embedded credentials, no loopback/private/link-local address.", + "description": "HTTPS URL that receives matches. Accepted: a publicly reachable HTTPS URL with no embedded credentials and no loopback, private or link-local address.", "format": "uri", "type": "string" }
--- pinned/reddit_monitor_webhook_delete +++ observed/reddit_monitor_webhook_delete { "annotations": { "destructiveHint": true, - "description": "Permanently delete a webhook. Any monitor still pointing at it will fail to deliver until repointed at a different webhook -- this does NOT cascade-delete or pause the monitors using it. Cannot be undone. Returns 404 `webhook_not_found` if the id does not exist or is not yours.", + "description": "Permanently deletes a webhook. A monitor still pointing at it fails to deliver until it is repointed at a different webhook: deletion does NOT cascade-delete or pause the monitors using it. Cannot be undone. Returns 404 `webhook_not_found` if the id does not exist or is not on this account.", "execution": { "taskSupport": "forbidden" }, - "description": "The webhook's id, from reddit_monitor_webhook_create's response or reddit_monitor_webhook_list.", + "description": "The webhook's id, as returned when the webhook was created or listed.", "minLength": 1, "type": "string" }
--- pinned/reddit_monitor_webhook_list +++ observed/reddit_monitor_webhook_list { "annotations": { "destructiveHint": false, - "description": "List every webhook registered on the caller's account. Never returns the signing secret (shown once, at creation, by reddit_monitor_webhook_create).", + "description": "Lists every webhook registered on the caller's account. The signing secret is not included; it is shown once, at creation.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_monitor_webhook_test +++ observed/reddit_monitor_webhook_test { "annotations": { "destructiveHint": false, - "description": "Send a one-off test delivery to a registered webhook (rate-limited to 10/min) so you can confirm it's wired up correctly before waiting for a real match. Uses the webhook's `kind` to format the test payload the same way a real delivery would. On failure the response carries `reason` and `status` plus TWO fields that say what to actually do: `hint`, our sentence naming the fix (most often that the target's `kind` does not match its host, which no test can succeed through), and `detail`, a bounded, sanitised copy of what the destination itself replied. Report `hint` to the user rather than the bare `reason` -- `http_error` with a 400 names no field, no value and no remedy. Returns 404 `webhook_not_found` if the id does not exist or is not yours, or `webhook_url_rejected` if the URL fails re-validation (e.g. now resolves to a private address).", + "description": "Sends a one-off test delivery to a registered webhook (rate-limited to 10/min), confirming it is wired up correctly before a real match arrives. The test payload is formatted by the webhook's `kind`, the same way a real delivery would be. On failure the response carries `reason` and `status`, plus `hint`, a sentence naming the fix (most often that the target's `kind` does not match its host, which no test can succeed through), and `detail`, a bounded, sanitised copy of what the destination itself replied. `hint` carries the remedy; a bare `reason` such as `http_error` with a 400 names no field, no value and no remedy. Returns 404 `webhook_not_found` if the id does not exist or is not on this account, or `webhook_url_rejected` if the URL fails re-validation (e.g. now resolves to a private address).", "execution": { "taskSupport": "forbidden" }, - "description": "The webhook's id, from reddit_monitor_webhook_create's response or reddit_monitor_webhook_list.", + "description": "The webhook's id, as returned when the webhook was created or listed.", "minLength": 1, "type": "string" }
--- pinned/reddit_post +++ observed/reddit_post { "annotations": { "destructiveHint": false, - "description": "Fetch a single Reddit post by its id. Returns the full post object (title, author, score, text, permalink, subreddit, url). Use when you already have a post id and want its details. Example: id='abc123' (the base-36 id, no t3_ prefix).", + "description": "Fetches a single Reddit post by its id. Returns the full post object (title, author, score, text, permalink, subreddit, url). Example: id='abc123' (the base-36 id, no t3_ prefix).", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_post_comments +++ observed/reddit_post_comments { "annotations": { "destructiveHint": false, - "description": "Fetch a single post and its comment tree by permalink. Returns the post plus threaded comments (author, body, score, replies) and an `after` cursor. Use after finding a post via search/listing to read the full discussion. Pass the post's `permalink` from a prior result.", + "description": "Fetches a single post and its comment tree by permalink. Returns the post plus threaded comments (author, body, score, replies) and an `after` cursor. The `permalink` is the one carried by any post result.", "execution": { "taskSupport": "forbidden" },
+ tool added: reddit_post_visibility
--- pinned/reddit_search +++ observed/reddit_search { "annotations": { "destructiveHint": false, - "description": "Search Reddit posts across all of Reddit or within one subreddit. Returns matching posts with author, score, comments, permalink, and an `after` cursor. Use for topic/keyword research, brand monitoring, or finding discussions. Scope to a community with `subreddit`. Optional advanced filters narrow the results by minimum/maximum score, comment count, media type, and post flags, with an optional re-sort of the page. Because filters are applied to the returned page, the response then carries a `meta` object with page-completeness counts, so a filtered result is never mistaken for the whole set; paginate with `after` to filter more. Example: q='rust vs go' sort='top' t='year'.", + "description": "Searches Reddit posts across all of Reddit or within one subreddit. Returns matching posts with author, score, comments, permalink, and an `after` cursor. `subreddit` scopes the search to one community. Optional advanced filters narrow the results by minimum/maximum score, comment count, media type, and post flags, with an optional re-sort of the page. Filters apply to the returned page, so a filtered response carries a `meta` object with page-completeness counts, and the next page comes from `after`. SORT BEHAVIOUR, measured 2026-09-11: `top`, `new` and `comments` order the MATCHING set by score, date or comment count alone, and Reddit matches loosely (image OCR, comments, and the word 'reddit' is in almost every big post), so a generic multi-word query with sort='top' and no subreddit returns the site-wide viral listing, not the topic. Even a distinctive term is ranked by score alone (q='pgvector' sort='top': 1 of 5 results was about pgvector, the rest were large posts that mention it once). Best match then highest score comes from sort='relevance' with a large `limit`, where sort_type='score' re-orders that returned page (it does not reach past the page); a quoted phrase (q='\"rust vs go\"') or a `subreddit` scope also narrows the matching set. Example: q='rust vs go' sort='relevance' t='year' limit=100 sort_type='score'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "contest_mode": { - "description": "Filter by the contest_mode flag.", + "description": "Filters by the contest_mode flag.", "type": "boolean" }, "is_self": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Filter by the locked flag.", + "description": "Filters by the locked flag.", "type": "boolean" }, "max_comments": { - "description": "Keep only posts with comment count <= this.", + "description": "Keeps only posts with comment count <= this.", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Keep only posts with score <= this.", + "description": "Keeps only posts with score <= this.", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Keep only posts with comment count >= this.", + "description": "Keeps only posts with comment count >= this.", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Keep only posts with score >= this (applied to the returned page).", + "description": "Keeps only posts with score >= this (applied to the returned page).", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false" - "description": "Filter the page by NSFW flag (distinct from nsfw, which controls inclusion in the search).", + "description": "Filters the page by NSFW flag (distinct from nsfw, which controls inclusion in the search).", "type": "boolean" }, "q": { - "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.", + "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed. Only 'relevance' weights how well a post matches; 'top', 'new' and 'comments' rank every loosely-matching post by that one number, so on a site-wide generic query 'top' returns viral posts that barely mention the terms. A quoted phrase or a `subreddit` scope narrows the matching set itself.", "enum": [ "relevance", "hot", - "description": "Re-sort the filtered page (descending) by this field.", + "description": "Re-sorts the filtered page (descending) by this field. Page-local: it re-orders only the posts this call returned, not the whole result set, so its reach is bounded by `limit`.", "enum": [ "score", "num_comments", - "description": "Filter by the spoiler flag.", + "description": "Filters by the spoiler flag.", "type": "boolean" }, "stickied": { - "description": "Filter by the stickied flag.", + "description": "Filters by the stickied flag.", "type": "boolean" }, "subreddit": { - "description": "Optional subreddit name (without r/) to restrict the search to one community. Omit to search all of Reddit.", + "description": "Optional subreddit name (without r/) that restricts the search to one community. Absent, the search covers all of Reddit.", "type": "string" }, "t": { - "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match. Pass 'week' or 'month' to keep results recent and on-topic.", + "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match; 'week' or 'month' keeps results recent and on topic.", "enum": [ "hour", "day",
--- pinned/reddit_search_comments +++ observed/reddit_search_comments { "annotations": { "destructiveHint": false, - "description": "Search Reddit by COMMENT text. Reddit's comment search matches your keyword against comment bodies but returns the PARENT POSTS, not the individual comments, so each result is a post whose discussion mentions your query, carrying that post's title, selftext, score, and comment count. Use it to surface threads where a topic comes up in the replies that plain post-title search would miss. Reddit does not expose which specific comment matched or its text, so this returns posts, not comment bodies. For the actual comment bodies, use reddit_deep_comment_search. Example: q='best mechanical keyboard' sort='top'.", + "description": "Searches Reddit by COMMENT text. Reddit's comment search matches the keyword against comment bodies but returns the PARENT POSTS, not the individual comments, so each result is a post whose discussion mentions the query, carrying that post's title, selftext, score, and comment count. It surfaces threads where a topic comes up in the replies, which a post-title search misses. Reddit does not expose which specific comment matched or its text, so the results are posts, not comment bodies. Example: q='best mechanical keyboard' sort='relevance' t='year'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false" - "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.", + "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed. Only 'relevance' weights how well a post matches; 'top', 'new' and 'comments' rank every loosely-matching post by that one number, so on a site-wide generic query 'top' returns viral posts that barely mention the terms. A quoted phrase or a `subreddit` scope narrows the matching set itself.", "enum": [ "relevance", "hot", - "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match. Pass 'week' or 'month' to keep results recent and on-topic.", + "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match; 'week' or 'month' keeps results recent and on topic.", "enum": [ "hour", "day",
--- pinned/reddit_search_communities +++ observed/reddit_search_communities { "annotations": { "destructiveHint": false, - "description": "Search for subreddits (communities) by name or topic. Returns matching subreddits with title, subscriber count, description, and NSFW flag. Use to discover where a topic is discussed before listing or searching its posts. Example: q='machine learning'.", + "description": "Searches for subreddits (communities) by name or topic. Returns matching subreddits with title, subscriber count, description, and NSFW flag. Example: q='machine learning'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false"
--- pinned/reddit_search_media +++ observed/reddit_search_media { "annotations": { "destructiveHint": false, - "description": "Search Reddit posts filtered to media (images, video, gifs). Returns media posts with the media URL/type, author, score, and the post url. Use `kind` to narrow to a media type. Example: q='aurora borealis' kind='image'.", + "description": "Searches Reddit posts filtered to media (images, video, gifs). Returns media posts with the media URL/type, author, score, and the post url. `kind` narrows the results to one media type. Example: q='aurora borealis' kind='image'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "kind": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false" - "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.", + "description": "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed. Only 'relevance' weights how well a post matches; 'top', 'new' and 'comments' rank every loosely-matching post by that one number, so on a site-wide generic query 'top' returns viral posts that barely mention the terms. A quoted phrase or a `subreddit` scope narrows the matching set itself.", "enum": [ "relevance", "hot", - "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match. Pass 'week' or 'month' to keep results recent and on-topic.", + "description": "Time window that bounds which posts the search returns, e.g. 'week' = only posts from the past week. Unlike a subreddit listing, search applies this to the 'relevance' and 'top' sorts too. When omitted, Reddit defaults to 'all', so a broad 'relevance' query surfaces old high-upvote posts that only loosely match; 'week' or 'month' keeps results recent and on topic.", "enum": [ "hour", "day",
--- pinned/reddit_search_users +++ observed/reddit_search_users { "annotations": { "destructiveHint": false, - "description": "Search for Reddit users (redditors) by name or keyword. Returns matching accounts with username, karma, and account age. Use to find a person's handle before fetching their profile or comments. Example: q='spez'.", + "description": "Searches for Reddit users (redditors) by name or keyword. Returns matching accounts with username, karma, and account age. Example: q='spez'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).", + "description": "'true' includes over-18 / NSFW results; 'false' or omitted excludes them (default).", "enum": [ "true", "false"
--- pinned/reddit_subreddit_about +++ observed/reddit_subreddit_about { "annotations": { "destructiveHint": false, - "description": "Fetch a subreddit's public metadata by name (Reddit's /r/<name>/about data). Returns the subreddit's title, public description, subscriber count, active-user count, creation timestamp, type, and NSFW flag. Use it to size or vet a community before listing or searching its posts. Example: name='python'.", + "description": "Fetches a subreddit's public metadata by name (Reddit's /r/<name>/about data). Returns the subreddit's title, public description, subscriber count, active-user count, creation timestamp, type, and NSFW flag. Example: name='python'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_subreddit_comments +++ observed/reddit_subreddit_comments { "annotations": { "destructiveHint": false, - "description": "Stream the NEWEST comments across an entire subreddit (Reddit's /r/<name>/comments feed), not one post's thread. Returns comments with body, author, score, subreddit, the parent post link, and timestamp, plus an `after` cursor. Poll it to catch new comments in a community as they are posted. Example: name='python'.", + "description": "Streams the NEWEST comments across an entire subreddit (Reddit's /r/<name>/comments feed), not one post's thread. Returns comments with body, author, score, subreddit, the parent post link, and timestamp, plus an `after` cursor. Repeated calls surface new comments in a community as they are posted. Example: name='python'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_subreddit_moderators +++ observed/reddit_subreddit_moderators { "annotations": { "destructiveHint": false, - "description": "Fetch a subreddit's moderator team by name (Reddit's /r/<name>/about/moderators data). Returns a `moderators` list, each with `name`, `id`, `mod_permissions`, `flair_text`, and `added` (when they joined the mod team). Use it to see who moderates a community. Example: name='python'.", + "description": "Fetches a subreddit's moderator team by name (Reddit's /r/<name>/about/moderators data). Returns a `moderators` list, each with `name`, `id`, `mod_permissions`, `flair_text`, and `added` (when they joined the mod team). Example: name='python'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_subreddit_posts +++ observed/reddit_subreddit_posts { "annotations": { "destructiveHint": false, - "description": "List posts from a subreddit by sort order. Use this to read a community's feed: newest, hot/trending, top-of-week, rising, etc. Returns post title, author, score, comment count, and permalink, plus an `after` cursor for paging. When `after` comes back null the response carries `listing_status`: `complete` means no older posts and is only claimed when the whole run came back in under one page, `truncated` means Reddit's cap cut you off, and `unknown` means we cannot tell, so do NOT report `unknown` as the end of the data. A busy feed that Reddit simply stops serving reports `unknown`, not `complete`, so treat `unknown` as an incomplete answer and widen across sorts, timeframes or search rather than paging deeper. Example: subreddit='programming' sort='top' t='week'.", + "description": "Lists posts from a subreddit by sort order: newest, hot, top of a time window, rising, controversial or best. Returns post title, author, score, comment count, and permalink, plus an `after` cursor for paging. When `after` comes back null the response carries `listing_status`: `complete` means no older posts and is only claimed when the whole run came back in under one page, `truncated` means Reddit's cap cut the listing off, and `unknown` means completeness could not be established. A busy feed that Reddit stops serving reports `unknown`, not `complete`. Example: subreddit='programming' sort='top' t='week'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_subreddit_rules +++ observed/reddit_subreddit_rules { "annotations": { "destructiveHint": false, - "description": "Fetch a subreddit's posting rules by name (Reddit's /r/<name>/about/rules data). Returns a `rules` list, each with name, description, what it applies to (posts, comments, or all), violation reason, priority, and creation date, plus a `site_rules` list of Reddit's site-wide rules. Use it to check a community's rules before posting or commenting. Example: name='python'.", + "description": "Fetches a subreddit's posting rules by name (Reddit's /r/<name>/about/rules data). Returns a `rules` list, each with name, description, what it applies to (posts, comments, or all), violation reason, priority, and creation date, plus a `site_rules` list of Reddit's site-wide rules. Example: name='python'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_subreddit_top +++ observed/reddit_subreddit_top { "annotations": { "destructiveHint": false, - "description": "Get the TOP posts of a subreddit for a time window. Shorthand for the highest-scoring posts of a community. Returns posts with score, author, comments, and permalink plus an `after` cursor. Example: name='science' t='month'.", + "description": "Gets the TOP posts of a subreddit for a time window, the highest-scoring posts of a community. Returns posts with score, author, comments, and permalink plus an `after` cursor. Example: name='science' t='month'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_subreddit_wiki +++ observed/reddit_subreddit_wiki { "annotations": { "destructiveHint": false, - "description": "Fetch a subreddit's wiki page by name and page (Reddit's /r/<name>/wiki/<page> data). Returns a single object with `content_md` and `content_html`, a `may_revise` flag, and the last revision (`revision_id`, `revision_date`, `revised_by`, `reason`). Use it to read a community's wiki, such as its rules or FAQ. The page may be multi-segment, for example index, rules, or config/sidebar. Example: name='python', page='index'.", + "description": "Fetches a subreddit's wiki page by name and page (Reddit's /r/<name>/wiki/<page> data). Returns a single object with `content_md` and `content_html`, a `may_revise` flag, and the last revision (`revision_id`, `revision_date`, `revised_by`, `reason`). A wiki commonly holds a community's rules or FAQ. The page may be multi-segment, for example index, rules, or config/sidebar. Example: name='python', page='index'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_subreddits_default +++ observed/reddit_subreddits_default { "annotations": { "destructiveHint": false, - "description": "Browse Reddit's default front-page set of subreddits, no keyword needed. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging. This BROWSES the default communities; use reddit_search_communities instead to SEARCH communities by keyword.", + "description": "Browses Reddit's default front-page set of subreddits, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_subreddits_new +++ observed/reddit_subreddits_new { "annotations": { "destructiveHint": false, - "description": "Browse the newest subreddits, the communities most recently created, no keyword needed. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging. This BROWSES communities by recency; use reddit_search_communities instead to SEARCH communities by keyword.", + "description": "Browses the newest subreddits, the communities most recently created, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_subreddits_popular +++ observed/reddit_subreddits_popular { "annotations": { "destructiveHint": false, - "description": "Browse the most-subscribed, trending subreddits right now, no keyword needed. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging. This BROWSES communities by popularity; use reddit_search_communities instead to SEARCH communities by keyword.", + "description": "Browses the most-subscribed, trending subreddits right now, with no keyword. Returns a `subreddits` list (each with name, title, subscriber count, description, type, and NSFW flag) plus an `after` cursor for paging.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
+ tool added: reddit_user_achievements
--- pinned/reddit_user_comments +++ observed/reddit_user_comments { "annotations": { "destructiveHint": false, - "description": "List a Reddit user's recent comments. Returns comments with body, score, subreddit, parent link, and timestamp plus an `after` cursor. Use to understand what a redditor talks about or to gather their opinions. Example: name='spez' sort='top'.", + "description": "Lists a Reddit user's recent comments. Returns comments with body, score, subreddit, parent link, and timestamp plus an `after` cursor. Example: name='spez' sort='top'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_user_gilded +++ observed/reddit_user_gilded { "annotations": { "destructiveHint": false, - "description": "List the posts and comments a Reddit account has received an award (gold) on. PRIVATE data -- Reddit only serves it to the account that owns it, so you must be logged in as `name` (see the `reddit_session`/`loid` args) or this returns 403. Mixed listing: each item is a post or a comment, tagged `kind`. Requires calling POST /api/reddit/login on the REST API first to get session cookies (not an MCP tool). Example: name='spez' (must match the logged-in account).", + "description": "Lists the posts and comments a Reddit account has received an award (gold) on. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "csrf_token": { - "description": "Reddit CSRF cookie, from the same login response. Not required for a read (CSRF only guards state-changing calls) but harmless to pass if you have it.", + "description": "Reddit CSRF cookie, from the same login response. Optional: a read is accepted without it (CSRF only guards state-changing calls).", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Reddit username WITHOUT the u/ prefix. Must be the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", + "description": "Reddit username WITHOUT the u/ prefix: the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", "minLength": 1, "type": "string" }, - "description": "Reddit account session cookie. Obtain it by calling POST /api/reddit/login on the REST API first (not an MCP tool) and reusing the `reddit_session` cookie it returns. Required.", + "description": "The Reddit account's session cookie, as returned by POST /api/reddit/login on the REST API (a REST call, not an MCP tool). Required.", "minLength": 1, "type": "string" },
--- pinned/reddit_user_hidden +++ observed/reddit_user_hidden { "annotations": { "destructiveHint": false, - "description": "List the posts and comments a Reddit account has HIDDEN. PRIVATE data -- Reddit only serves it to the account that owns it, so you must be logged in as `name` (see the `reddit_session`/`loid` args) or this returns 403. Mixed listing: each item is a post or a comment, tagged `kind`. Requires calling POST /api/reddit/login on the REST API first to get session cookies (not an MCP tool). Example: name='spez' (must match the logged-in account).", + "description": "Lists the posts and comments a Reddit account has HIDDEN. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "csrf_token": { - "description": "Reddit CSRF cookie, from the same login response. Not required for a read (CSRF only guards state-changing calls) but harmless to pass if you have it.", + "description": "Reddit CSRF cookie, from the same login response. Optional: a read is accepted without it (CSRF only guards state-changing calls).", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Reddit username WITHOUT the u/ prefix. Must be the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", + "description": "Reddit username WITHOUT the u/ prefix: the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", "minLength": 1, "type": "string" }, - "description": "Reddit account session cookie. Obtain it by calling POST /api/reddit/login on the REST API first (not an MCP tool) and reusing the `reddit_session` cookie it returns. Required.", + "description": "The Reddit account's session cookie, as returned by POST /api/reddit/login on the REST API (a REST call, not an MCP tool). Required.", "minLength": 1, "type": "string" },
--- pinned/reddit_user_profile +++ observed/reddit_user_profile { "annotations": { "destructiveHint": false, - "description": "Fetch a Reddit user's public profile by username. Returns account info: username, id, karma (post + comment), account age, verified/employee flags, and avatar. Use to vet or summarize a redditor. Example: name='spez'.", + "description": "Fetches a Reddit user's public profile by username. Returns account info: username, id, karma (post + comment), account age, verified/employee flags, and avatar. Example: name='spez'.", "execution": { "taskSupport": "forbidden" },
--- pinned/reddit_user_saved +++ observed/reddit_user_saved { "annotations": { "destructiveHint": false, - "description": "List the posts and comments a Reddit account has SAVED. PRIVATE data -- Reddit only serves it to the account that owns it, so you must be logged in as `name` (see the `reddit_session`/`loid` args) or this returns 403. Mixed listing: each item is a post or a comment, tagged `kind`. Requires calling POST /api/reddit/login on the REST API first to get session cookies (not an MCP tool). Example: name='spez' (must match the logged-in account).", + "description": "Lists the posts and comments a Reddit account has SAVED. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "csrf_token": { - "description": "Reddit CSRF cookie, from the same login response. Not required for a read (CSRF only guards state-changing calls) but harmless to pass if you have it.", + "description": "Reddit CSRF cookie, from the same login response. Optional: a read is accepted without it (CSRF only guards state-changing calls).", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Reddit username WITHOUT the u/ prefix. Must be the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", + "description": "Reddit username WITHOUT the u/ prefix: the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", "minLength": 1, "type": "string" }, - "description": "Reddit account session cookie. Obtain it by calling POST /api/reddit/login on the REST API first (not an MCP tool) and reusing the `reddit_session` cookie it returns. Required.", + "description": "The Reddit account's session cookie, as returned by POST /api/reddit/login on the REST API (a REST call, not an MCP tool). Required.", "minLength": 1, "type": "string" },
--- pinned/reddit_user_submitted +++ observed/reddit_user_submitted { "annotations": { "destructiveHint": false, - "description": "List a Reddit user's submitted POSTS (their post history, the sibling of reddit_user_comments). Returns posts with title, author, score, comment count, and permalink, plus an `after` cursor. Use to see what a redditor posts, not just what they comment on. Example: name='spez' sort='top'.", + "description": "Lists a Reddit user's submitted POSTS (their post history, as distinct from their comments). Returns posts with title, author, score, comment count, and permalink, plus an `after` cursor. Example: name='spez' sort='top'.", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer"
--- pinned/reddit_user_upvoted +++ observed/reddit_user_upvoted { "annotations": { "destructiveHint": false, - "description": "List the posts and comments a Reddit account has UPVOTED. PRIVATE data -- Reddit only serves it to the account that owns it, so you must be logged in as `name` (see the `reddit_session`/`loid` args) or this returns 403. Mixed listing: each item is a post or a comment, tagged `kind`. Requires calling POST /api/reddit/login on the REST API first to get session cookies (not an MCP tool). Example: name='spez' (must match the logged-in account).", + "description": "Lists the posts and comments a Reddit account has UPVOTED. PRIVATE data: Reddit serves it only to the account that owns it, so the request carries that account's Reddit session cookies (the session and loid arguments, from POST /api/reddit/login on the REST API, which is not an MCP tool) and returns 403 when `name` is a different account. Mixed listing: each item is a post or a comment, tagged `kind`. Example: name='spez' (the logged-in account).", "execution": { "taskSupport": "forbidden" }, - "description": "Opaque pagination cursor. Pass back the previous response's `after` value EXACTLY as it was returned; the format is not stable and a hand-written Reddit fullname loses the paging depth the cursor carries. Omit on the first call. When `after` comes back null there is no next page to request, and that does NOT reliably mean you have every item: Reddit often stops serving a busy listing long before it runs out. Read `listing_status` on that final response instead. It is `complete`, `truncated` or `unknown`, and ONLY `complete` means nothing is missing. Never report `truncated` or `unknown` to a user as the end of the data; say the answer is partial and widen across sorts, timeframes or search terms.", + "description": "Opaque pagination cursor: the previous response's `after` value, exactly as it was returned. The format is not stable, and a hand-written Reddit fullname loses the paging depth the cursor carries. Absent on the opening page. A null `after` on a response means there is no next page; it does not by itself mean the listing is complete, because Reddit often stops serving a busy listing long before it runs out. That final response carries `listing_status`: `complete`, `truncated` or `unknown`, and only `complete` means nothing is missing.", "type": "string" }, "csrf_token": { - "description": "Reddit CSRF cookie, from the same login response. Not required for a read (CSRF only guards state-changing calls) but harmless to pass if you have it.", + "description": "Reddit CSRF cookie, from the same login response. Optional: a read is accepted without it (CSRF only guards state-changing calls).", "type": "string" }, "limit": { - "description": "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.", + "description": "Maximum items returned, 1 to 100. The API clamps out-of-range values; the endpoint default applies when omitted.", "maximum": 100, "minimum": 1, "type": "integer" - "description": "Reddit username WITHOUT the u/ prefix. Must be the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", + "description": "Reddit username WITHOUT the u/ prefix: the SAME account the supplied cookies belong to, or Reddit returns 403. Required (path parameter).", "minLength": 1, "type": "string" }, - "description": "Reddit account session cookie. Obtain it by calling POST /api/reddit/login on the REST API first (not an MCP tool) and reusing the `reddit_session` cookie it returns. Required.", + "description": "The Reddit account's session cookie, as returned by POST /api/reddit/login on the REST API (a REST call, not an MCP tool). Required.", "minLength": 1, "type": "string" },
+ tool added: reddit_verify_comments
Current tools
Show all 47 tool fingerprints
Watch this server yourself
If you run this server, put the proxy in front of it. It pins these exact fingerprints on first connect and stops the session if they move.
npx -y mcp-pin@0.2.3 -- <your redditapis-mcp command>
Or subscribe to this page's RSS feed to be told when it changes.
Badge
The badge states one fact about time and nothing else. It never claims a server is safe.
[](https://mcp-pin.gautamkhosla.com/servers/8005455a463a00e9.html)