redditapis-mcp 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+ - **Fix: the server no longer exits at startup when `REDDITAPIS_KEY` is missing.**
5
+ A registry connectivity scanner (Smithery, Glama, the official MCP registry,
6
+ Claude Connectors Directory) spins up the server with no real credential to
7
+ enumerate `tools/list`. Exiting before the transport connects made every
8
+ automated scan fail outright and read as a generic connectivity error rather
9
+ than a missing-key error, which is most of why this listing scored low on
10
+ every registry's capability-quality checks. Tools now register and
11
+ `tools/list` responds regardless of whether a key is present; an actual tool
12
+ call with no key still fails clearly, at the point of the call, with the
13
+ same style of message the existing 401 branch already used.
14
+ - Adds `server.json` (official MCP server manifest: description, homepage,
15
+ repository, npm package identifier, and the `REDDITAPIS_KEY` config field
16
+ with a title and description) to the repo and to the published package, so
17
+ registry submissions have a durable, versioned source instead of a local,
18
+ never-committed file.
19
+
20
+ ## 0.3.0
21
+ - 4 new READ tools for the caller's own private Reddit listings (upvoted,
22
+ saved, hidden, gilded), each requiring the caller's own Reddit session
23
+ cookie obtained via `POST /api/reddit/login`.
24
+ - Pagination docs clarified across every listing tool: `listing_status` on the
25
+ final page distinguishes `complete` (nothing missing) from `truncated`/
26
+ `unknown` (Reddit stopped serving a busy listing early), so a client no
27
+ longer reads a null `after` cursor as proof of a complete result set.
28
+ - This CHANGELOG entry is retroactive; 0.3.0 published without one, along with
29
+ drifting one npm publish ahead of what GitHub had committed. Both are
30
+ corrected as part of the 0.3.1 pass.
31
+
3
32
  ## 0.2.0
4
33
  - **10 new monitor/webhook management tools** (task #43), the first WRITES this
5
34
  catalog has ever carried. Unlike the reddit comment/vote/DM writes this MCP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redditapis-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.redditapis/redditapis-mcp",
5
5
  "description": "Official MCP server for redditapis.com, the Reddit API (subreddit listings, post + comment + community + user + media search, comment trees, top posts, user profiles) plus monitor/webhook management (create/list/update/remove a monitor, register/test/delete a webhook, read delivery history) as native tools for Claude, Cursor, and any MCP client.",
6
6
  "repository": {
@@ -18,7 +18,8 @@
18
18
  "README.md",
19
19
  "LICENSE",
20
20
  "CHANGELOG.md",
21
- "smithery.yaml"
21
+ "smithery.yaml",
22
+ "server.json"
22
23
  ],
23
24
  "engines": {
24
25
  "node": ">=18"
package/server.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.redditapis/redditapis-mcp",
4
+ "description": "Reddit API as MCP tools: search, subreddits, posts, comments, users, monitors, and webhooks.",
5
+ "title": "RedditAPIs",
6
+ "version": "0.3.1",
7
+ "websiteUrl": "https://www.redditapis.com",
8
+ "repository": {
9
+ "url": "https://github.com/redditapis/redditapis-mcp",
10
+ "source": "github"
11
+ },
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "redditapis-mcp",
16
+ "version": "0.3.1",
17
+ "transport": {
18
+ "type": "stdio"
19
+ },
20
+ "environmentVariables": [
21
+ {
22
+ "name": "REDDITAPIS_KEY",
23
+ "description": "Your redditapis.com API key. Get one at https://www.redditapis.com",
24
+ "isRequired": true,
25
+ "isSecret": true
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
package/src/index.js CHANGED
@@ -33,11 +33,19 @@ const REQUEST_TIMEOUT_MS = Number(process.env.REDDITAPIS_TIMEOUT_MS || 30000);
33
33
  // User-Agent and MCP server handshake understated its own version for months.
34
34
  const { version: VERSION } = createRequire(import.meta.url)("../package.json");
35
35
 
36
+ // Lazy validation, not exit-on-boot: an MCP registry scanner (Smithery, Glama,
37
+ // the official registry, Claude Connectors) connects the stdio transport with
38
+ // no real credential to enumerate tools/list. Exiting here before the server
39
+ // ever registers a tool makes that handshake fail outright and reads as a
40
+ // generic connectivity error, not a missing-key error, on the scanner side --
41
+ // confirmed live 2026-08-18 (Smithery: "Initialization failed... could not be
42
+ // automatically scanned", HTTP 404). Warn and continue; a real tool CALL made
43
+ // with no key still fails clearly, at the point of the call, same as it
44
+ // already does for a bad key (see the 401 branch below).
36
45
  if (!API_KEY) {
37
46
  console.error(
38
- "[reddit-mcp] Missing REDDITAPIS_KEY. Get a key at https://www.redditapis.com and set it in your MCP client config.",
47
+ "[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.",
39
48
  );
40
- process.exit(1);
41
49
  }
42
50
 
43
51
  // REST call. Resolves any {param} path placeholders from args, and for GET
@@ -46,6 +54,15 @@ if (!API_KEY) {
46
54
  // `filterSpecFields`). Forwards the API key as a Bearer token (redditapis.com
47
55
  // authenticates via `Authorization: Bearer <key>` only).
48
56
  async function callEndpoint(pathTemplate, args, method = "GET", tool = null) {
57
+ if (!API_KEY) {
58
+ return {
59
+ isError: true,
60
+ content: [{
61
+ type: "text",
62
+ text: "Missing REDDITAPIS_KEY (invalid or missing API key, get one at https://www.redditapis.com and set it in your MCP client config).",
63
+ }],
64
+ };
65
+ }
49
66
  const { path, rest } = buildPath(pathTemplate, args);
50
67
  const isWrite = method !== "GET";
51
68
  const q = isWrite ? "" : buildQuery(rest);
package/src/tools.js CHANGED
@@ -126,6 +126,12 @@ const MONITOR_SUBREDDIT = {
126
126
  subreddit: z.array(z.string().min(1)).min(1).max(50).optional().describe(MONITOR_SUBREDDIT_DESC),
127
127
  };
128
128
  const MONITOR_FILTER_FIELDS = {
129
+ // SITEWIDE ONLY BY DESIGN, and the description says so in the first clause
130
+ // because an agent that passes it alongside `subreddit` gets a 400 and needs
131
+ // to know from the schema, not from the error, which of the two to drop.
132
+ exclude_subreddits: z.array(z.string().min(1)).max(50).optional().describe(
133
+ "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.",
134
+ ),
129
135
  q: z.string().max(200).optional().describe(
130
136
  "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.",
131
137
  ),
@@ -519,7 +525,7 @@ export const TOOLS = [
519
525
  path: "/api/reddit/monitor/add",
520
526
  method: "POST",
521
527
  write: true,
522
- filterSpecFields: ["subreddit", "kind", "q", "author", "exclude_terms", "domain", "include_any", "include_all", "search_in", "group", "min_score", "nsfw"],
528
+ filterSpecFields: ["subreddit", "exclude_subreddits", "kind", "q", "author", "exclude_terms", "domain", "include_any", "include_all", "search_in", "group", "min_score", "nsfw"],
523
529
  description:
524
530
  "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.",
525
531
  shape: {
@@ -546,7 +552,7 @@ export const TOOLS = [
546
552
  path: "/api/reddit/monitor/update",
547
553
  method: "POST",
548
554
  write: true,
549
- filterSpecFields: ["subreddit", "kind", "q", "author", "exclude_terms", "domain", "include_any", "include_all", "search_in", "group", "min_score", "nsfw"],
555
+ filterSpecFields: ["subreddit", "exclude_subreddits", "kind", "q", "author", "exclude_terms", "domain", "include_any", "include_all", "search_in", "group", "min_score", "nsfw"],
550
556
  description:
551
557
  "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.",
552
558
  shape: {