redditapis-mcp 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +4 -2
- package/package.json +2 -2
- package/src/index.js +30 -2
- package/src/tools.js +16 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-09-11)
|
|
4
|
+
|
|
5
|
+
A minor rather than a patch because a tool was added: anyone pinned to 0.5.x opts in rather than receiving it silently, the same rule the 0.5.0 and 0.4.0 entries below state.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`reddit_user_achievements`.** A user's public achievements, the trophies on their reddit.com/user/<name>/achievements page, with name, description, granted timestamp and icons. An account with none returns an empty list, so a zero count is a real answer.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **`reddit_search` no longer steers you into site-wide `sort=top`.** Reddit orders `top`, `new` and `comments` by that one number over a LOOSELY matched set (OCR image text and comments count, and the word "reddit" is in almost every large post), so a generic multi-word query with `sort=top` and no `subreddit` returned the site-wide viral listing rather than the topic. Measured 2026-09-11: `q=reddit api pricing`, `sort=top`, `t=year` returned r/MadeMeSmile at 107k upvotes and a bathroom remodel; `sort=relevance` on the same query returned API-pricing threads; the quoted phrase returned exact matches only; `q=pgvector` was on topic on any sort. The tool description and the `sort` enum text now say only `relevance` weights match quality and point at `sort=relevance` plus `sort_type=score`, a quoted phrase, or a `subreddit` scope. The worked example no longer models `sort=top`.
|
|
14
|
+
|
|
15
|
+
### Housekeeping
|
|
16
|
+
|
|
17
|
+
- The authoring repo had fallen behind the published package: 0.5.3 shipped from the org repo on 2026-09-06 without landing here, so `src/index.js` in this repo still exited on a missing key. That change and its test are now back in the authoring repo, and this release is cut from it.
|
|
18
|
+
|
|
19
|
+
## 0.5.3 (2026-09-06)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **A registry scanner can now read the tool catalog without a key.** `src/index.js` used to `process.exit(1)` when `REDDITAPIS_KEY` was unset, so a directory that spawns the server to enumerate its tools got no answer to `initialize` and nothing from `tools/list`, and listed the server as uninspectable. Measured 2026-09-06 by speaking raw MCP stdio to the published 0.5.2 with an empty env. The server now warns and continues; a tool CALL with no key fails at the call with a clear message, and no `Bearer undefined` header is ever sent. Not a security relaxation: this is a stdio server, whoever spawns it already has local execution. Test: `test/scannable-without-key.test.mjs`. (This entry was written on 2026-09-11; the 0.5.3 release carried no changelog line.)
|
|
24
|
+
|
|
3
25
|
## 0.5.2 (2026-09-06)
|
|
4
26
|
|
|
5
27
|
### Added
|
package/README.md
CHANGED
|
@@ -151,6 +151,7 @@ A few conventions across the catalog:
|
|
|
151
151
|
| `reddit_user_profile` | `GET /api/reddit/user/{name}` | A user's public profile: karma, account age, verified/employee flags, avatar. |
|
|
152
152
|
| `reddit_user_comments` | `GET /api/reddit/user/{name}/comments` | A user's recent comments (body, score, subreddit, parent link, timestamp). |
|
|
153
153
|
| `reddit_user_submitted` | `GET /api/reddit/user/{name}/submitted` | A user's submitted posts (the sibling of `reddit_user_comments`). |
|
|
154
|
+
| `reddit_user_achievements` | `GET /api/reddit/user/{name}/achievements` | A user's public achievements (the trophies on their profile): name, description, granted timestamp, icons. An account with none returns an empty list. |
|
|
154
155
|
|
|
155
156
|
### Community browse (no keyword)
|
|
156
157
|
|
|
@@ -197,8 +198,9 @@ The agent calls `reddit_search` with:
|
|
|
197
198
|
|
|
198
199
|
```
|
|
199
200
|
q: "borrow checker"
|
|
200
|
-
sort: "
|
|
201
|
+
sort: "relevance"
|
|
201
202
|
t: "month"
|
|
203
|
+
sort_type: "score"
|
|
202
204
|
```
|
|
203
205
|
|
|
204
206
|
### Read a community's top posts of the week
|
|
@@ -223,7 +225,7 @@ The agent calls `reddit_deep_comment_search` with:
|
|
|
223
225
|
```
|
|
224
226
|
q: "mechanical keyboard"
|
|
225
227
|
group_by: "author"
|
|
226
|
-
sort: "
|
|
228
|
+
sort: "relevance"
|
|
227
229
|
```
|
|
228
230
|
|
|
229
231
|
Research mode returns the distinct people who mentioned the query, ranked by how many of their comments matched, each with their top comment and the subreddits they matched in.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redditapis-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "io.github.redditapis/redditapis-mcp",
|
|
5
5
|
"description": "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.",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"start": "node src/index.js",
|
|
28
28
|
"check": "node --check src/index.js && node --check src/tools.js && node --check src/feedback.js",
|
|
29
|
-
"test": "node test/tools.test.mjs && node test/feedback.test.mjs && node test/index-lasterror.test.mjs",
|
|
29
|
+
"test": "node test/tools.test.mjs && node test/feedback.test.mjs && node test/index-lasterror.test.mjs && node test/scannable-without-key.test.mjs",
|
|
30
30
|
"prepublishOnly": "node scripts/prepublish-tenant-check.mjs"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
package/src/index.js
CHANGED
|
@@ -39,11 +39,25 @@ const REQUEST_TIMEOUT_MS = Number(process.env.REDDITAPIS_TIMEOUT_MS || 30000);
|
|
|
39
39
|
// User-Agent and MCP server handshake understated its own version for months.
|
|
40
40
|
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
41
41
|
|
|
42
|
+
// WARN AND CONTINUE, NEVER exit. A registry scanner spawns the server and calls
|
|
43
|
+
// tools/list to enumerate what it offers, and tools/list is the DISCOVERY step
|
|
44
|
+
// in the MCP spec: it comes before tool selection and before any invocation, so
|
|
45
|
+
// it needs no credential. Exiting here answers that scan with nothing.
|
|
46
|
+
//
|
|
47
|
+
// This is not a hypothesis. Measured 2026-09-06 by speaking raw MCP stdio to
|
|
48
|
+
// the published redditapis-mcp@0.5.2 with an empty env: initialize was never
|
|
49
|
+
// answered and tools/list returned nothing, because this line fired first. A
|
|
50
|
+
// scanner that behaves this way reports the server as uninspectable rather than
|
|
51
|
+
// as having no tools, so the listing carries no catalog at all.
|
|
52
|
+
//
|
|
53
|
+
// NOT a security relaxation. This is a stdio server: whoever spawns it already
|
|
54
|
+
// has local execution, so there is no unauthenticated party to expose anything
|
|
55
|
+
// to. A real tool CALL with no key still fails clearly, at the point of the
|
|
56
|
+
// call, exactly as it already does for a WRONG key (see the 401 branch below).
|
|
42
57
|
if (!API_KEY) {
|
|
43
58
|
console.error(
|
|
44
|
-
"[reddit-mcp] Missing REDDITAPIS_KEY. Get a key at https://www.redditapis.com and set it in your MCP client config.",
|
|
59
|
+
"[reddit-mcp] Missing REDDITAPIS_KEY. Get a key at https://www.redditapis.com and set it in your MCP client config. Tools are registered but every call will fail until it is set.",
|
|
45
60
|
);
|
|
46
|
-
process.exit(1);
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
// The last tool call that failed, so a feedback draft can carry the endpoint,
|
|
@@ -90,6 +104,20 @@ export function hintFor(status, path) {
|
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
async function callEndpoint(pathTemplate, args, method = "GET", tool = null) {
|
|
107
|
+
// The credential check MOVED HERE from startup, so listing tools needs no key
|
|
108
|
+
// and calling one still cannot silently proceed without it. Without this the
|
|
109
|
+
// relaxation above would send an Authorization header reading "Bearer
|
|
110
|
+
// undefined" and the caller would read a 401 about an INVALID key when the
|
|
111
|
+
// real answer is that no key was ever set.
|
|
112
|
+
if (!API_KEY) {
|
|
113
|
+
return {
|
|
114
|
+
isError: true,
|
|
115
|
+
content: [{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: "Missing REDDITAPIS_KEY (no API key is set; get one at https://www.redditapis.com and set it in your MCP client config).",
|
|
118
|
+
}],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
93
121
|
const { path, rest: pathRest } = buildPath(pathTemplate, args);
|
|
94
122
|
// A tool that declares `sessionHeaders` has its Reddit session args lifted
|
|
95
123
|
// out of the query and onto headers. For every other tool this is the
|
package/src/tools.js
CHANGED
|
@@ -61,7 +61,7 @@ const SORT_POSTS = {
|
|
|
61
61
|
};
|
|
62
62
|
const SORT_SEARCH = {
|
|
63
63
|
sort: z.enum(["relevance", "hot", "top", "new", "comments"]).optional().describe(
|
|
64
|
-
"Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.",
|
|
64
|
+
"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. Prefer 'relevance', a quoted phrase, or a `subreddit` scope.",
|
|
65
65
|
),
|
|
66
66
|
};
|
|
67
67
|
const SORT_USER = {
|
|
@@ -265,7 +265,7 @@ export const TOOLS = [
|
|
|
265
265
|
name: "reddit_search",
|
|
266
266
|
path: "/api/reddit/search",
|
|
267
267
|
description:
|
|
268
|
-
"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='
|
|
268
|
+
"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. SORT CAVEAT, 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). For best-match-then-highest-score, fetch with sort='relevance' and a large `limit`, then sort_type='score' re-orders that returned page (it never reaches past the page), or quote the phrase (q='\"rust vs go\"'), or scope with `subreddit`. Example: q='rust vs go' sort='relevance' t='year' limit=100 sort_type='score'.",
|
|
269
269
|
shape: {
|
|
270
270
|
...QUERY,
|
|
271
271
|
subreddit: z.string().optional().describe(
|
|
@@ -287,7 +287,7 @@ export const TOOLS = [
|
|
|
287
287
|
stickied: z.boolean().optional().describe("Filter by the stickied flag."),
|
|
288
288
|
spoiler: z.boolean().optional().describe("Filter by the spoiler flag."),
|
|
289
289
|
contest_mode: z.boolean().optional().describe("Filter by the contest_mode flag."),
|
|
290
|
-
sort_type: z.enum(["score", "num_comments", "created"]).optional().describe("Re-sort the filtered page (descending) by this field."),
|
|
290
|
+
sort_type: z.enum(["score", "num_comments", "created"]).optional().describe("Re-sort the filtered page (descending) by this field. Page-local: it re-orders only the posts this call returned, never the whole result set, so pair it with a large `limit`."),
|
|
291
291
|
},
|
|
292
292
|
},
|
|
293
293
|
{
|
|
@@ -327,14 +327,14 @@ export const TOOLS = [
|
|
|
327
327
|
name: "reddit_search_comments",
|
|
328
328
|
path: "/api/reddit/search/comments",
|
|
329
329
|
description:
|
|
330
|
-
"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='
|
|
330
|
+
"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='relevance' t='year'.",
|
|
331
331
|
shape: { ...QUERY, ...SORT_SEARCH, ...TIME_SEARCH, ...AFTER, ...NSFW, ...LIMIT },
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
name: "reddit_deep_comment_search",
|
|
335
335
|
path: "/api/reddit/search/comments/deep",
|
|
336
336
|
description:
|
|
337
|
-
"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='
|
|
337
|
+
"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='relevance' t='year'.",
|
|
338
338
|
shape: {
|
|
339
339
|
...QUERY,
|
|
340
340
|
...SORT_SEARCH,
|
|
@@ -415,6 +415,17 @@ export const TOOLS = [
|
|
|
415
415
|
),
|
|
416
416
|
},
|
|
417
417
|
},
|
|
418
|
+
{
|
|
419
|
+
name: "reddit_user_achievements",
|
|
420
|
+
path: "/api/reddit/user/{name}/achievements",
|
|
421
|
+
description:
|
|
422
|
+
"List 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. Use to gauge account age and standing (One-Year Club, Verified Email) or to check a moderator's history. Example: name='spez'.",
|
|
423
|
+
shape: {
|
|
424
|
+
name: z.string().min(1).describe(
|
|
425
|
+
"Reddit username WITHOUT the u/ prefix (e.g. 'spez'). Required (path parameter).",
|
|
426
|
+
),
|
|
427
|
+
},
|
|
428
|
+
},
|
|
418
429
|
{
|
|
419
430
|
name: "reddit_user_comments",
|
|
420
431
|
path: "/api/reddit/user/{name}/comments",
|