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 +26 -0
- package/README.md +2 -2
- package/package.json +7 -2
- package/src/server.js +46 -3
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 });
|