redditapis-mcp 0.1.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 ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ - Initial release: 11 read tools over the redditapis.com API (subreddit listings,
5
+ post/comment/community/user/media search, comment tree, subreddit top posts,
6
+ user profile + comments). Stdio transport, Bearer auth, per-request timeout.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 redditapis.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # redditapis-mcp
2
+
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ **[redditapis.com](https://www.redditapis.com)** — the Reddit API as native tools
5
+ for Claude, Cursor, and any MCP client.
6
+
7
+ It exposes Reddit reads as typed tools: subreddit listings, post / comment /
8
+ community / user / media search, a post's full comment tree, subreddit top posts,
9
+ and user profile + comments. Each tool is a thin, stateless wrapper over a REST
10
+ endpoint at `https://api.redditapis.com`; your API key is forwarded on every call.
11
+
12
+ ## Setup
13
+
14
+ Get an API key at [redditapis.com](https://www.redditapis.com), then add the
15
+ server to your MCP client. It runs over stdio via `npx` (no install needed):
16
+
17
+ ### Claude Desktop / Claude Code / Cursor (`mcp.json`)
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "reddit": {
23
+ "command": "npx",
24
+ "args": ["-y", "redditapis-mcp@latest"],
25
+ "env": { "REDDITAPIS_KEY": "your_api_key_here" }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Configuration (env)
32
+
33
+ | Variable | Required | Default | Notes |
34
+ |---|---|---|---|
35
+ | `REDDITAPIS_KEY` | yes | — | Your key. `REDDIT_APIS_KEY` is accepted as an alias. |
36
+ | `REDDITAPIS_BASE_URL` | no | `https://api.redditapis.com` | Override the API base. |
37
+ | `REDDITAPIS_TIMEOUT_MS` | no | `30000` | Per-request timeout in ms. |
38
+
39
+ ## Tools
40
+
41
+ All tools are **read-only**. Reddit writes (comment / vote / DM) are a separate
42
+ authenticated surface and are intentionally not exposed here.
43
+
44
+ | Tool | Endpoint | What it does |
45
+ |---|---|---|
46
+ | `reddit_subreddit_posts` | `GET /api/reddit/posts` | List a subreddit's posts by sort (hot/new/top/rising/…) |
47
+ | `reddit_search` | `GET /api/reddit/search` | Search posts across Reddit or one subreddit |
48
+ | `reddit_post_comments` | `GET /api/reddit/comments` | A post + its comment tree, by permalink |
49
+ | `reddit_search_communities` | `GET /api/reddit/search/communities` | Find subreddits by name/topic |
50
+ | `reddit_search_comments` | `GET /api/reddit/search/comments` | Search comments (not posts) |
51
+ | `reddit_search_media` | `GET /api/reddit/search/media` | Search image/video/gif posts |
52
+ | `reddit_search_users` | `GET /api/reddit/search/users` | Find redditors by name |
53
+ | `reddit_subreddit_top` | `GET /api/reddit/sub/{name}/top` | Top posts of a subreddit for a time window |
54
+ | `reddit_post` | `GET /api/reddit/post/{id}` | A single post by id |
55
+ | `reddit_user_profile` | `GET /api/reddit/user/{name}` | A user's public profile |
56
+ | `reddit_user_comments` | `GET /api/reddit/user/{name}/comments` | A user's recent comments |
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ npm install
62
+ npm run check # syntax check both source files
63
+ npm test # unit-test the tool catalog + query/path builders (no network)
64
+ npm start # run the stdio server (needs REDDITAPIS_KEY)
65
+ ```
66
+
67
+ ## License
68
+
69
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "redditapis-mcp",
3
+ "version": "0.1.1",
4
+ "description": "Official MCP server for redditapis.com, the Reddit API (subreddit listings, post + comment + community + user + media search, comment trees, top posts, user profiles) as native tools for Claude, Cursor, and any MCP client.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/redditapis/redditapis-mcp.git"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "reddit-mcp": "src/index.js",
12
+ "redditapis-mcp": "src/index.js"
13
+ },
14
+ "main": "src/index.js",
15
+ "files": [
16
+ "src",
17
+ "README.md",
18
+ "LICENSE",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "start": "node src/index.js",
26
+ "check": "node --check src/index.js && node --check src/tools.js",
27
+ "test": "node test/tools.test.mjs"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.0",
31
+ "zod": "^3.23.8 || ^4.0.0"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "reddit",
37
+ "reddit-api",
38
+ "claude",
39
+ "cursor",
40
+ "redditapis"
41
+ ],
42
+ "author": "redditapis.com",
43
+ "license": "MIT",
44
+ "homepage": "https://www.redditapis.com",
45
+ "bugs": {
46
+ "url": "https://github.com/redditapis/redditapis-mcp/issues"
47
+ }
48
+ }
package/src/index.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // redditapis-mcp, official MCP server for redditapis.com
3
+ //
4
+ // Exposes the Reddit API as native MCP tools for Claude, Cursor, and any MCP
5
+ // client: subreddit listings, post + comment search, community/user/media/comment
6
+ // search, a post's comment tree, subreddit top posts, and user profile/comments.
7
+ // Each tool is a thin, typed wrapper over a REST endpoint at
8
+ // https://api.redditapis.com. The server holds no state and forwards your API
9
+ // key on every call. The tool catalog lives in ./tools.js.
10
+ //
11
+ // Config (env):
12
+ // REDDITAPIS_KEY required. Your key from https://www.redditapis.com
13
+ // (REDDIT_APIS_KEY is accepted as an alias).
14
+ // REDDITAPIS_BASE_URL optional. Defaults to https://api.redditapis.com
15
+ // REDDITAPIS_TIMEOUT_MS optional. Per-request timeout (default 30000)
16
+ //
17
+ // Run: npx -y redditapis-mcp@latest (stdio transport)
18
+
19
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+ import { TOOLS, buildQuery, buildPath } from "./tools.js";
22
+
23
+ const API_KEY = process.env.REDDITAPIS_KEY || process.env.REDDIT_APIS_KEY;
24
+ const BASE_URL = (
25
+ process.env.REDDITAPIS_BASE_URL || "https://api.redditapis.com"
26
+ ).replace(/\/+$/, "");
27
+ const REQUEST_TIMEOUT_MS = Number(process.env.REDDITAPIS_TIMEOUT_MS || 30000);
28
+ const VERSION = "0.1.0";
29
+
30
+ if (!API_KEY) {
31
+ console.error(
32
+ "[reddit-mcp] Missing REDDITAPIS_KEY. Get a key at https://www.redditapis.com and set it in your MCP client config.",
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ // REST call. Resolves any {param} path placeholders from args, sends the rest as
38
+ // the query string, and forwards the API key as a Bearer token (redditapis.com
39
+ // authenticates via `Authorization: Bearer <key>` only).
40
+ async function callEndpoint(pathTemplate, args, method = "GET") {
41
+ const { path, rest } = buildPath(pathTemplate, args);
42
+ const q = buildQuery(rest);
43
+ const url = `${BASE_URL}${path}${q ? `?${q}` : ""}`;
44
+
45
+ const headers = {
46
+ Authorization: `Bearer ${API_KEY}`,
47
+ accept: "application/json",
48
+ "user-agent": `reddit-mcp/${VERSION}`,
49
+ };
50
+
51
+ const ctrl = new AbortController();
52
+ const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
53
+ try {
54
+ const res = await fetch(url, { method, headers, signal: ctrl.signal });
55
+ const body = await res.text();
56
+ if (!res.ok) {
57
+ const hint =
58
+ res.status === 401
59
+ ? " (invalid or missing API key, verify REDDITAPIS_KEY at https://www.redditapis.com)"
60
+ : res.status === 402
61
+ ? " (insufficient credits, top up at https://www.redditapis.com)"
62
+ : res.status === 403
63
+ ? " (access forbidden. The subreddit/user may be private, banned, or quarantined)"
64
+ : res.status === 404
65
+ ? " (not found. The subreddit, post id, user, or permalink may be wrong or deleted)"
66
+ : res.status === 429
67
+ ? " (rate limited. Wait a few seconds and retry, or reduce request frequency)"
68
+ : res.status >= 500
69
+ ? " (upstream API error. Retry in a moment; if persistent, contact https://www.redditapis.com)"
70
+ : "";
71
+ return { isError: true, content: [{ type: "text", text: `HTTP ${res.status}${hint}: ${body.slice(0, 1200)}` }] };
72
+ }
73
+ return { content: [{ type: "text", text: body }] };
74
+ } catch (err) {
75
+ const msg = err?.name === "AbortError" ? `timed out after ${REQUEST_TIMEOUT_MS}ms` : err?.message || String(err);
76
+ return { isError: true, content: [{ type: "text", text: `Request failed: ${msg}` }] };
77
+ } finally {
78
+ clearTimeout(timer);
79
+ }
80
+ }
81
+
82
+ const server = new McpServer({ name: "redditapis", version: VERSION });
83
+
84
+ for (const tool of TOOLS) {
85
+ const method = tool.method || "GET";
86
+ const annotations = {
87
+ title: tool.name,
88
+ readOnlyHint: !tool.write, // every tool here is a read
89
+ destructiveHint: Boolean(tool.destructive),
90
+ openWorldHint: true,
91
+ };
92
+ server.registerTool(
93
+ tool.name,
94
+ { description: tool.description, inputSchema: tool.shape, annotations },
95
+ async (args) => callEndpoint(tool.path, args, method),
96
+ );
97
+ }
98
+
99
+ async function main() {
100
+ const transport = new StdioServerTransport();
101
+ await server.connect(transport);
102
+ // Logs go to stderr so they never corrupt the stdio JSON-RPC stream.
103
+ console.error(`[reddit-mcp] ready · ${TOOLS.length} tools · base ${BASE_URL}`);
104
+ }
105
+
106
+ main().catch((err) => {
107
+ console.error("[reddit-mcp] fatal:", err);
108
+ process.exit(1);
109
+ });
package/src/tools.js ADDED
@@ -0,0 +1,215 @@
1
+ // Tool catalog + pure query/path builders for redditapis-mcp.
2
+ // Kept separate from the server wiring (index.js) so it can be unit-tested
3
+ // without spawning the stdio transport.
4
+ //
5
+ // Each tool maps 1:1 to a REST endpoint at https://api.redditapis.com/api/reddit.
6
+ // Tool arg names map 1:1 to endpoint query params, EXCEPT path params written as
7
+ // {name}/{id} in a tool's `path` (e.g. /api/reddit/sub/{name}/top), which are
8
+ // interpolated into the URL and removed from the query string. Every tool here
9
+ // is a READ (GET); reddit writes (comment/vote/DM) are a separate authenticated
10
+ // surface and are intentionally out of scope for this reads MCP.
11
+ import { z } from "zod";
12
+
13
+ // Shared Zod input-schema fragments.
14
+ const LIMIT = {
15
+ limit: z.number().int().min(1).max(100).optional().describe(
16
+ "Max items to return (1 to 100). The API clamps out-of-range values; endpoint default applies if omitted.",
17
+ ),
18
+ };
19
+ const AFTER = {
20
+ after: z.string().optional().describe(
21
+ "Opaque Reddit pagination cursor from the previous response's `after` field (a fullname like `t3_abc123`). Omit on the first call; pass it to fetch the next page.",
22
+ ),
23
+ };
24
+ // Sort vocabularies differ by endpoint (they map to different Reddit listings):
25
+ // posts/listings, search, and user-comments each accept a distinct set.
26
+ const SORT_POSTS = {
27
+ sort: z.enum(["new", "hot", "top", "rising", "controversial", "best"]).optional().describe(
28
+ "Sort order for a subreddit listing. 'hot' = trending now, 'new' = most recent (default), 'top' = highest score in the `t` window, 'rising' = gaining fast, 'controversial' = polarizing, 'best' = Reddit's blended rank.",
29
+ ),
30
+ };
31
+ const SORT_SEARCH = {
32
+ sort: z.enum(["relevance", "hot", "top", "new", "comments"]).optional().describe(
33
+ "Sort order for search. 'relevance' = best match (default), 'top' = highest score in the `t` window, 'new' = most recent, 'hot' = trending, 'comments' = most-discussed.",
34
+ ),
35
+ };
36
+ const SORT_USER = {
37
+ sort: z.enum(["new", "hot", "top", "controversial"]).optional().describe(
38
+ "Sort order for a user's comments. 'new' = most recent (default), 'hot', 'top' (in the `t` window), 'controversial'.",
39
+ ),
40
+ };
41
+ const TIME = {
42
+ t: z.enum(["hour", "day", "week", "month", "year", "all"]).optional().describe(
43
+ "Time window, only applied when sort is 'top' or 'controversial'. E.g. 'week' = top of the past week. Ignored for other sorts.",
44
+ ),
45
+ };
46
+ const NSFW = {
47
+ nsfw: z.enum(["true", "false"]).optional().describe(
48
+ "Set 'true' to include over-18 / NSFW results. Omit or 'false' to exclude them (default).",
49
+ ),
50
+ };
51
+ const QUERY = {
52
+ q: z.string().min(1).describe(
53
+ "Search query text. Supports Reddit search syntax (e.g. `subreddit:webdev`, `author:spez`, `\"exact phrase\"`, `title:...`).",
54
+ ),
55
+ };
56
+
57
+ // The tool catalog. All reads (GET). Path params are {name}/{id} placeholders.
58
+ export const TOOLS = [
59
+ {
60
+ name: "reddit_subreddit_posts",
61
+ path: "/api/reddit/posts",
62
+ description:
63
+ "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, permalink, flair, and media, plus an `after` cursor for paging. Example: subreddit='programming' sort='top' t='week'.",
64
+ shape: {
65
+ subreddit: z.string().min(1).describe(
66
+ "Subreddit name WITHOUT the r/ prefix (e.g. 'programming', 'AskReddit'). Required.",
67
+ ),
68
+ ...SORT_POSTS,
69
+ ...TIME,
70
+ ...AFTER,
71
+ ...LIMIT,
72
+ },
73
+ },
74
+ {
75
+ name: "reddit_search",
76
+ path: "/api/reddit/search",
77
+ description:
78
+ "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`. Example: q='rust vs go' sort='top' t='year'.",
79
+ shape: {
80
+ ...QUERY,
81
+ subreddit: z.string().optional().describe(
82
+ "Optional subreddit name (without r/) to restrict the search to one community. Omit to search all of Reddit.",
83
+ ),
84
+ ...SORT_SEARCH,
85
+ ...TIME,
86
+ ...AFTER,
87
+ ...NSFW,
88
+ ...LIMIT,
89
+ },
90
+ },
91
+ {
92
+ name: "reddit_post_comments",
93
+ path: "/api/reddit/comments",
94
+ description:
95
+ "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.",
96
+ shape: {
97
+ permalink: z.string().min(1).describe(
98
+ "The post permalink path from a prior post result, e.g. '/r/programming/comments/abc123/some_title/'. Required.",
99
+ ),
100
+ },
101
+ },
102
+ {
103
+ name: "reddit_search_communities",
104
+ path: "/api/reddit/search/communities",
105
+ description:
106
+ "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'.",
107
+ shape: { ...QUERY, ...AFTER, ...NSFW, ...LIMIT },
108
+ },
109
+ {
110
+ name: "reddit_search_comments",
111
+ path: "/api/reddit/search/comments",
112
+ description:
113
+ "Search Reddit COMMENTS (not posts) by keyword across Reddit. Returns matching comments with author, body, score, subreddit, and the parent post link. Use to find first-hand opinions/answers buried in threads. Example: q='best mechanical keyboard' sort='top'.",
114
+ shape: { ...QUERY, ...SORT_SEARCH, ...TIME, ...AFTER, ...NSFW, ...LIMIT },
115
+ },
116
+ {
117
+ name: "reddit_search_media",
118
+ path: "/api/reddit/search/media",
119
+ description:
120
+ "Search Reddit posts filtered to media (images, video, gifs). Returns media posts with the media URL/type, author, score, and permalink. Use `kind` to narrow to a media type. Example: q='aurora borealis' kind='image'.",
121
+ shape: {
122
+ ...QUERY,
123
+ kind: z.enum(["image", "video", "gif", "all"]).optional().describe(
124
+ "Media type filter. 'image', 'video', 'gif', or 'all' (default). Filters the raw Reddit results to that media kind.",
125
+ ),
126
+ ...SORT_SEARCH,
127
+ ...TIME,
128
+ ...AFTER,
129
+ ...NSFW,
130
+ ...LIMIT,
131
+ },
132
+ },
133
+ {
134
+ name: "reddit_search_users",
135
+ path: "/api/reddit/search/users",
136
+ description:
137
+ "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'.",
138
+ shape: { ...QUERY, ...AFTER, ...NSFW, ...LIMIT },
139
+ },
140
+ {
141
+ name: "reddit_subreddit_top",
142
+ path: "/api/reddit/sub/{name}/top",
143
+ description:
144
+ "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'.",
145
+ shape: {
146
+ name: z.string().min(1).describe(
147
+ "Subreddit name WITHOUT the r/ prefix (e.g. 'science'). Required (path parameter).",
148
+ ),
149
+ ...TIME,
150
+ ...AFTER,
151
+ ...LIMIT,
152
+ },
153
+ },
154
+ {
155
+ name: "reddit_post",
156
+ path: "/api/reddit/post/{id}",
157
+ description:
158
+ "Fetch a single Reddit post by its id. Returns the full post object (title, author, score, body/selftext, media, permalink, subreddit). Use when you already have a post id and want its details. Example: id='abc123' (the base-36 id, no t3_ prefix).",
159
+ shape: {
160
+ id: z.string().min(1).describe(
161
+ "The post's base-36 id (e.g. 'abc123'), without the 't3_' fullname prefix. Required (path parameter).",
162
+ ),
163
+ },
164
+ },
165
+ {
166
+ name: "reddit_user_profile",
167
+ path: "/api/reddit/user/{name}",
168
+ description:
169
+ "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'.",
170
+ shape: {
171
+ name: z.string().min(1).describe(
172
+ "Reddit username WITHOUT the u/ prefix (e.g. 'spez'). Required (path parameter).",
173
+ ),
174
+ },
175
+ },
176
+ {
177
+ name: "reddit_user_comments",
178
+ path: "/api/reddit/user/{name}/comments",
179
+ description:
180
+ "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'.",
181
+ shape: {
182
+ name: z.string().min(1).describe(
183
+ "Reddit username WITHOUT the u/ prefix (e.g. 'spez'). Required (path parameter).",
184
+ ),
185
+ ...SORT_USER,
186
+ ...AFTER,
187
+ ...LIMIT,
188
+ },
189
+ },
190
+ ];
191
+
192
+ // Turn tool args into a URL query string. Skips undefined/null/empty values.
193
+ export function buildQuery(args) {
194
+ const qs = new URLSearchParams();
195
+ for (const [k, v] of Object.entries(args || {})) {
196
+ if (v !== undefined && v !== null && String(v).length > 0) qs.set(k, String(v));
197
+ }
198
+ return qs.toString();
199
+ }
200
+
201
+ // Interpolate {param} path placeholders from args; return the resolved path and
202
+ // the remaining args (which become the query string). A missing path param is
203
+ // left empty (the endpoint then 404s, surfaced to the caller) rather than
204
+ // throwing, so one bad call never crashes the server.
205
+ export function buildPath(template, args) {
206
+ const used = new Set();
207
+ const path = String(template).replace(/\{(\w+)\}/g, (_, k) => {
208
+ used.add(k);
209
+ const v = args?.[k];
210
+ return encodeURIComponent(v == null ? "" : String(v));
211
+ });
212
+ const rest = {};
213
+ for (const [k, v] of Object.entries(args || {})) if (!used.has(k)) rest[k] = v;
214
+ return { path, rest };
215
+ }