redditapis-mcp 0.7.0 → 0.8.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1 (2026-09-30)
4
+
5
+ ### Fixed
6
+
7
+ - **The README's tool counts match the catalog.** It said 34 tools (22 reads, 10
8
+ monitor/webhook, 2 feedback); the server has shipped 44 since the feedback and
9
+ account tools landed: 30 Reddit reads, 10 monitor/webhook management tools, 3
10
+ feedback tools and `reddit_account_me`. No code change.
11
+
12
+ ## 0.8.0 (2026-09-29)
13
+
14
+ ### Added
15
+
16
+ - **Remote hosting.** `redditapis-mcp/server` exports `createServer`, and an
17
+ `authHeaders` option authenticates calls with headers the host supplies
18
+ instead of an API key, for a remote MCP endpoint that has already signed the
19
+ caller in (a connected app over OAuth).
20
+
21
+ ### Changed
22
+
23
+ - **A read rides out an API restart.** A read that gets the gateway's HTML
24
+ 502/503, or a refused connection, is retried after 3 and then 8
25
+ seconds. The API's own JSON errors, gateway timeouts, DNS or TLS failures
26
+ and every write are never retried, so a request the API may already have
27
+ handled is not sent twice.
28
+
3
29
  ## 0.7.0 (2026-09-29)
4
30
 
5
31
  ### Added
package/README.md CHANGED
@@ -100,7 +100,7 @@ Authentication is a Bearer token: the server sends `Authorization: Bearer <REDDI
100
100
 
101
101
  ## Tools
102
102
 
103
- 34 tools: 22 reads, 10 monitor/webhook management tools, and 2 feedback tools. Reddit writes (posting, commenting, voting, DMs) remain a separate authenticated surface and are intentionally out of scope here -- monitor/webhook tools configure your OWN redditapis.com account (an alerting subscription), never Reddit itself, and the feedback tools send a report to the redditapis.com team, never to Reddit. Every read works with just your API key; the 6 monitor/webhook writes additionally need an active monitoring plan (see Monitoring below). The feedback tools are free and need only your key.
103
+ 44 tools: 30 Reddit reads, 10 monitor/webhook management tools, 3 feedback tools, and 1 account tool (`reddit_account_me`). Reddit writes (posting, commenting, voting, DMs) remain a separate authenticated surface and are intentionally out of scope here -- monitor/webhook tools configure your OWN redditapis.com account (an alerting subscription), never Reddit itself, and the feedback tools send a report to the redditapis.com team, never to Reddit. Every read works with just your API key; the 6 monitor/webhook writes additionally need an active monitoring plan (see Monitoring below). The feedback tools are free and need only your key.
104
104
 
105
105
  A few conventions across the catalog:
106
106
 
@@ -303,7 +303,7 @@ npm start # run the stdio server (needs REDDITAPIS_KEY)
303
303
 
304
304
  **Do I need a Reddit developer account?** No. Get an API key at [redditapis.com](https://www.redditapis.com); there is no application or approval step.
305
305
 
306
- **Can it post, comment, or vote?** No. All 22 Reddit-facing tools read Reddit; posting, commenting, voting, and DMs are a separate authenticated surface and are not exposed here. The other 10 tools manage your OWN redditapis.com monitors/webhooks, which is a write, but never a write to Reddit itself.
306
+ **Can it post, comment, or vote?** No. All 30 Reddit-facing tools read Reddit; posting, commenting, voting, and DMs are a separate authenticated surface and are not exposed here. Of the other 14, the 10 monitor/webhook tools manage your OWN redditapis.com monitors and webhooks (a write, but never a write to Reddit itself), the 3 feedback tools send or read a report to the redditapis.com team, and `reddit_account_me` reads your own balance and plan.
307
307
 
308
308
  **Which clients are supported?** Claude Desktop, Claude Code, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
309
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redditapis-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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": {
@@ -13,6 +13,11 @@
13
13
  "redditapis-mcp": "src/index.js"
14
14
  },
15
15
  "main": "src/index.js",
16
+ "exports": {
17
+ ".": "./src/index.js",
18
+ "./server": "./src/server.js",
19
+ "./package.json": "./package.json"
20
+ },
16
21
  "files": [
17
22
  "src",
18
23
  "README.md",
@@ -26,7 +31,7 @@
26
31
  "scripts": {
27
32
  "start": "node src/index.js",
28
33
  "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 && node test/scannable-without-key.test.mjs && node test/per-caller-state.test.mjs && node test/paywall.test.mjs",
34
+ "test": "node test/tools.test.mjs && node test/feedback.test.mjs && node test/index-lasterror.test.mjs && node test/scannable-without-key.test.mjs && node test/per-caller-state.test.mjs && node test/paywall.test.mjs && node test/remote-host.test.mjs",
30
35
  "prepublishOnly": "node scripts/prepublish-tenant-check.mjs"
31
36
  },
32
37
  "dependencies": {
package/src/server.js CHANGED
@@ -157,6 +157,14 @@ export const INSTRUCTIONS =
157
157
  * (REDDITAPIS_FEEDBACK_DIR); a remote host gives each
158
158
  * caller its own directory
159
159
  * @param {typeof fetch} [opts.fetchImpl] injectable for tests
160
+ * @param {Record<string,string>} [opts.authHeaders]
161
+ * headers that authenticate each call INSTEAD of the API key, for a host
162
+ * that already authenticated the caller another way (a connected app's
163
+ * OAuth token resolved to an account) and forwards calls over its own
164
+ * trusted channel. When set, no API key is required or sent.
165
+ * @param {(ms:number)=>Promise<void>} [opts.sleepImpl] injectable for tests
166
+ * @param {number[]} [opts.retryDelaysMs] waits before each retry of a read that
167
+ * hit a gateway failure (an API restart); default [3000, 8000]
160
168
  */
161
169
  export function createServer({
162
170
  apiKey,
@@ -164,6 +172,9 @@ export function createServer({
164
172
  timeoutMs = DEFAULT_TIMEOUT_MS,
165
173
  feedbackEnv = process.env,
166
174
  fetchImpl = fetch,
175
+ authHeaders = null,
176
+ sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
177
+ retryDelaysMs = [3000, 8000],
167
178
  } = {}) {
168
179
  const BASE_URL = String(baseUrl).replace(/\/+$/, "");
169
180
  const REQUEST_TIMEOUT_MS = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
@@ -185,7 +196,7 @@ export function createServer({
185
196
  // the request would carry an Authorization header reading "Bearer
186
197
  // undefined" and the caller would read a 401 about an INVALID key when the
187
198
  // real answer is that no key was ever set.
188
- if (!apiKey) return paywallResult("no_key");
199
+ if (!apiKey && !authHeaders) return paywallResult("no_key");
189
200
  const { path, rest: pathRest } = buildPath(pathTemplate, args);
190
201
  // A tool that declares `sessionHeaders` has its Reddit session args lifted
191
202
  // out of the query and onto headers. For every other tool this is the
@@ -196,7 +207,7 @@ export function createServer({
196
207
  const url = `${BASE_URL}${path}${q ? `?${q}` : ""}`;
197
208
 
198
209
  const headers = {
199
- Authorization: `Bearer ${apiKey}`,
210
+ ...(authHeaders ? authHeaders : { Authorization: `Bearer ${apiKey}` }),
200
211
  accept: "application/json",
201
212
  "user-agent": `reddit-mcp/${VERSION}`,
202
213
  ...sessionHeaders,
@@ -207,11 +218,34 @@ export function createServer({
207
218
  requestBody = JSON.stringify(buildBody(tool || {}, rest));
208
219
  }
209
220
 
221
+ // An API restart makes the gateway answer an HTML 502/503 or refuse the
222
+ // connection for a short window. A READ is retried through it; the API's own
223
+ // JSON errors, timeouts and writes never are, so a request the API may already have
224
+ // handled is never sent twice (a gateway 504 is not retried for that reason).
225
+ // Only a REFUSED connection is retried: the request never reached the API,
226
+ // so it cannot have been handled or billed. A reset, a broken pipe or a
227
+ // socket dropped mid-answer can all happen after the API did the work, and
228
+ // "fetch failed" alone also covers DNS and TLS errors that retrying cannot fix.
229
+ const RETRYABLE_NET = new Set(["ECONNREFUSED"]);
230
+ const gatewayFailure = (status, text) =>
231
+ (status === 502 || status === 503) && /^\s*<(!doctype|html)/i.test(text);
232
+ const transientNetwork = (err) =>
233
+ !gotResponse && err?.name !== "AbortError" && RETRYABLE_NET.has(err?.cause?.code ?? err?.code);
234
+ let attempt = 0;
235
+ let gotResponse = false;
236
+ for (;;) {
237
+ gotResponse = false;
210
238
  const ctrl = new AbortController();
211
239
  const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
212
240
  try {
213
241
  const res = await fetchImpl(url, { method, headers, body: requestBody, signal: ctrl.signal });
242
+ gotResponse = true;
214
243
  const body = await res.text();
244
+ if (!isWrite && attempt < retryDelaysMs.length && gatewayFailure(res.status, body)) {
245
+ clearTimeout(timer);
246
+ await sleepImpl(retryDelaysMs[attempt++]);
247
+ continue;
248
+ }
215
249
  if (!res.ok) {
216
250
  const hint = hintFor(res.status, path);
217
251
  lastError = {
@@ -242,12 +276,21 @@ export function createServer({
242
276
  lastError = null;
243
277
  return { content: [{ type: "text", text: body }] };
244
278
  } catch (err) {
245
- const msg = err?.name === "AbortError" ? `timed out after ${REQUEST_TIMEOUT_MS}ms` : err?.message || String(err);
279
+ if (!isWrite && attempt < retryDelaysMs.length && transientNetwork(err)) {
280
+ clearTimeout(timer);
281
+ await sleepImpl(retryDelaysMs[attempt++]);
282
+ continue;
283
+ }
284
+ const code = err?.cause?.code ?? err?.code;
285
+ const msg = err?.name === "AbortError"
286
+ ? `timed out after ${REQUEST_TIMEOUT_MS}ms`
287
+ : `${err?.message || String(err)}${code ? ` (${code})` : ""}`;
246
288
  lastError = { path, method, status: null, error: msg.slice(0, 200), ts: Date.now() };
247
289
  return { isError: true, content: [{ type: "text", text: `Request failed: ${msg}` }] };
248
290
  } finally {
249
291
  clearTimeout(timer);
250
292
  }
293
+ }
251
294
  }
252
295
 
253
296
  const server = new McpServer({ name: "redditapis", version: VERSION }, { instructions: INSTRUCTIONS });