@mcpware/reddit-unofficial-api 0.1.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/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # Reddit Unofficial API — MCP Server
2
+
3
+ Free Reddit API. No API keys, no OAuth registration, no monthly fees.
4
+
5
+ All you need is Chrome with Reddit logged in, and Chrome DevTools Protocol enabled.
6
+
7
+ ## How it works
8
+
9
+ This MCP server talks to Reddit through your browser session. When you're logged into Reddit in Chrome, your browser already has all the authentication it needs. This server simply executes `fetch()` calls in your Reddit tab's context, using the same cookies your browser uses.
10
+
11
+ ```
12
+ AI Assistant → this MCP server → Chrome DevTools Protocol → your Reddit tab → Reddit's internal API
13
+ ```
14
+
15
+ Reddit's internal API (the same endpoints their website uses) gives you everything: read posts, write comments, vote, search, send messages, manage subscriptions. All free, all through your existing session.
16
+
17
+ ## Setup
18
+
19
+ ### 1. Start Chrome with DevTools Protocol enabled
20
+
21
+ ```bash
22
+ # macOS
23
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
24
+
25
+ # Linux
26
+ google-chrome --remote-debugging-port=9222
27
+
28
+ # Windows
29
+ "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
30
+ ```
31
+
32
+ ### 2. Log into Reddit in Chrome
33
+
34
+ Just open reddit.com and log in normally.
35
+
36
+ ### 3. Add to your MCP config
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "reddit": {
42
+ "command": "npx",
43
+ "args": ["-y", "@mcpware/reddit-unofficial-api"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ If Chrome is on a non-default host/port:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "reddit": {
55
+ "command": "npx",
56
+ "args": ["-y", "@mcpware/reddit-unofficial-api"],
57
+ "env": {
58
+ "CHROME_CDP_HOST": "localhost",
59
+ "CHROME_CDP_PORT": "9222"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Available Tools
67
+
68
+ ### Read (no rate limit concerns)
69
+
70
+ | Tool | What it does |
71
+ |------|-------------|
72
+ | `reddit_me` | Who am I? Get your user info |
73
+ | `reddit_get_post` | Get a post and its comments |
74
+ | `reddit_get_comments` | Get all comments as a flat list (great for analysis) |
75
+ | `reddit_get_user` | Look up any user's profile, posts, or comments |
76
+ | `reddit_get_subreddit` | Get subreddit info, rules, or browse posts |
77
+ | `reddit_search` | Search across Reddit or within a subreddit |
78
+ | `reddit_get_inbox` | Read your messages |
79
+ | `reddit_get_subscriptions` | List your subscribed subreddits |
80
+
81
+ ### Write (1.5s rate limit between calls)
82
+
83
+ | Tool | What it does |
84
+ |------|-------------|
85
+ | `reddit_comment` | Reply to any post or comment |
86
+ | `reddit_submit` | Create a new post (text or link) |
87
+ | `reddit_vote` | Upvote, downvote, or unvote |
88
+ | `reddit_save` | Save/unsave posts or comments |
89
+ | `reddit_send_message` | Send a private message |
90
+ | `reddit_edit` | Edit your own content |
91
+ | `reddit_delete` | Delete your own content |
92
+ | `reddit_subscribe` | Join or leave a subreddit |
93
+
94
+ ### Utility
95
+
96
+ | Tool | What it does |
97
+ |------|-------------|
98
+ | `reddit_ensure_session` | Verify everything is connected and you're logged in |
99
+
100
+ ## What can you do with this?
101
+
102
+ ### Community Management
103
+ - **Monitor your posts**: "check my PokeClaw post for new comments that need replies" → scrapes all comments, categorizes them (bug report, question, feature request, positive feedback), drafts replies, you approve, it posts
104
+ - **Batch reply**: reply to 10+ comments in one go with personalized responses, auto-verified to make sure each reply goes to the right person
105
+ - **Update your posts**: edit the body of an existing post to add changelogs, new links, or corrections
106
+
107
+ ### Research & Competitive Intelligence
108
+ - **Search Reddit**: find all posts mentioning your product, a competitor, or a topic across any subreddit
109
+ - **User research**: look up anyone's post history, comment history, karma breakdown
110
+ - **Subreddit discovery**: find relevant subreddits for your niche, check their rules before posting
111
+
112
+ ### Content & Marketing
113
+ - **Cross-post to multiple subreddits**: draft a post, you approve, submit to r/LocalLLaMA, r/androiddev, r/machinelearning, etc.
114
+ - **Track engagement**: check which of your posts/comments are getting upvoted or replied to
115
+ - **Save interesting threads**: bookmark posts and comments for later reference
116
+
117
+ ### Inbox & Messaging
118
+ - **Read DMs**: check if anyone messaged you about your project
119
+ - **Send DMs**: reach out to users who want to contribute or reported bugs
120
+ - **Mark as read**: clean up your inbox
121
+
122
+ ### Engagement
123
+ - **Upvote helpful feedback**: upvote users who gave useful bug reports or feature suggestions
124
+ - **Subscribe to subreddits**: join relevant communities from your AI assistant
125
+
126
+ ### Deep Research
127
+ - **Topic deep-dive**: "research what Reddit thinks about on-device LLM apps" → searches multiple subreddits, reads top posts and comment threads, summarizes sentiment, common complaints, feature requests, and what users actually want
128
+ - **Competitor analysis**: "what are people saying about DroidRun vs OpenClaw" → finds all relevant threads, extracts comparisons, user experiences, pros/cons mentioned
129
+ - **Market validation**: "is there demand for WhatsApp auto-reply bots" → searches across subreddits, analyzes upvotes and engagement, identifies the audience and their pain points
130
+ - **Trend tracking**: "what local LLM topics are trending this week on r/LocalLLaMA" → fetches top/rising posts, identifies emerging themes
131
+
132
+ Everything runs through your existing browser session. Zero cost, no API keys, no rate limit surprises.
133
+
134
+ ## Why not just use the official Reddit API?
135
+
136
+ Reddit killed free API access in 2023. The official API now requires app registration, OAuth flows, and has strict rate limits. If you're building something for personal use with your own account, jumping through those hoops makes no sense.
137
+
138
+ This approach uses the exact same endpoints Reddit's own website uses. Your browser is already authenticated. We just let your AI assistant use that same session.
139
+
140
+ ## Safety Guidelines — Don't Get Your Account Banned
141
+
142
+ This tool uses your real Reddit account. Reddit can't tell the difference between you typing and this tool typing, because it's the same browser session. That's the whole point. But it also means you need to behave like a human.
143
+
144
+ **Why the risk is low:**
145
+ - Requests come from your normal browser with your normal cookies. To Reddit's servers, it looks identical to you clicking buttons manually.
146
+ - The server adds delays between write operations (1.5s minimum). You're not hammering their API.
147
+ - Content is genuine (you write and approve the replies), not auto-generated spam.
148
+
149
+ **Rules to follow:**
150
+ - **Don't batch reply too aggressively.** 10-15 replies in one session is fine. 50 replies in 10 minutes is not. Spread it out across a few hours if you have a lot to respond to.
151
+ - **Vary your reply content.** If every reply has the exact same structure, links, and call-to-action, Reddit's spam filter will notice. Make each reply address the specific person's point.
152
+ - **Mix in normal behavior.** Browse, upvote a few things, read some posts. Don't make your entire session activity look like a bot replying to everything.
153
+ - **Don't use this for spam, vote manipulation, or astroturfing.** Obviously. This is for managing your own community and doing research, not gaming Reddit.
154
+ - **Respect subreddit rules.** Some subs don't allow self-promotion. Check before cross-posting.
155
+
156
+ **What happens if Reddit flags you:**
157
+ - Most likely: temporary rate limit (can't post for a few hours)
158
+ - Less likely: shadowban (your replies only visible to you, check at r/ShadowBan)
159
+ - Very unlikely: account suspension (only for obvious spam or manipulation)
160
+
161
+ **Recovery:**
162
+ - If rate limited, just wait. It resets.
163
+ - If shadowbanned, appeal at reddit.com/appeal. Usually reversed if your content is genuine.
164
+ - Best prevention: keep it natural. If a human wouldn't post 30 comments in 5 minutes, don't do it with this tool either.
165
+
166
+ ## Limitations
167
+
168
+ - Needs Chrome running with DevTools Protocol (port 9222)
169
+ - Needs you to be logged into Reddit in that Chrome instance
170
+ - One Reddit account at a time (whichever is logged in)
171
+ - Rate limits are undocumented but generous for normal use
172
+ - Not suitable for high-volume scraping or bot armies (and you shouldn't do that anyway)
173
+
174
+ ## Install from npm
175
+
176
+ ```bash
177
+ npm install -g @mcpware/reddit-unofficial-api
178
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/index.js ADDED
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import CDP from "chrome-remote-interface";
6
+ // Reddit Unofficial API — MCP Server
7
+ // Executes fetch() in Chrome's Reddit tab context via CDP
8
+ // Requires: Chrome with --remote-debugging-port=9222 + Reddit tab logged in
9
+ const server = new McpServer({
10
+ name: "reddit-unofficial-api",
11
+ version: "0.1.0",
12
+ });
13
+ // ==================== Configuration ====================
14
+ const CDP_HOST = process.env.CHROME_CDP_HOST || "localhost";
15
+ const CDP_PORT = parseInt(process.env.CHROME_CDP_PORT || "9222", 10);
16
+ const RATE_LIMIT_MS = 1500;
17
+ let lastWriteTime = 0;
18
+ async function rateLimit() {
19
+ const now = Date.now();
20
+ const elapsed = now - lastWriteTime;
21
+ if (elapsed < RATE_LIMIT_MS) {
22
+ await new Promise((r) => setTimeout(r, RATE_LIMIT_MS - elapsed));
23
+ }
24
+ lastWriteTime = Date.now();
25
+ }
26
+ // ==================== CDP Connection ====================
27
+ let cachedClient = null;
28
+ let cachedTargetId = null;
29
+ async function findRedditTarget() {
30
+ let targets;
31
+ try {
32
+ targets = await CDP.List({ host: CDP_HOST, port: CDP_PORT });
33
+ }
34
+ catch (e) {
35
+ throw new Error(`Cannot connect to Chrome DevTools at ${CDP_HOST}:${CDP_PORT}. ` +
36
+ `Start Chrome with: google-chrome --remote-debugging-port=${CDP_PORT}`);
37
+ }
38
+ const redditTab = targets.find((t) => t.type === "page" && t.url?.includes("reddit.com"));
39
+ if (!redditTab) {
40
+ throw new Error("No Reddit tab found in Chrome. Open reddit.com and log in first.");
41
+ }
42
+ return redditTab;
43
+ }
44
+ async function getClient() {
45
+ // Reuse connection if target hasn't changed
46
+ if (cachedClient && cachedTargetId) {
47
+ try {
48
+ // Quick health check — if the tab closed, this will fail
49
+ await cachedClient.Runtime.evaluate({ expression: "1" });
50
+ return cachedClient;
51
+ }
52
+ catch {
53
+ // Connection stale, reconnect
54
+ try {
55
+ await cachedClient.close();
56
+ }
57
+ catch {
58
+ /* already dead */
59
+ }
60
+ cachedClient = null;
61
+ cachedTargetId = null;
62
+ }
63
+ }
64
+ const target = await findRedditTarget();
65
+ const client = await CDP({ host: CDP_HOST, port: CDP_PORT, target });
66
+ await client.Runtime.enable();
67
+ cachedClient = client;
68
+ cachedTargetId = target.id;
69
+ return client;
70
+ }
71
+ async function evalInReddit(script) {
72
+ const client = await getClient();
73
+ const result = await client.Runtime.evaluate({
74
+ expression: script,
75
+ awaitPromise: true,
76
+ returnByValue: true,
77
+ });
78
+ if (result.exceptionDetails) {
79
+ const msg = result.exceptionDetails.text ||
80
+ result.result?.description ||
81
+ "Unknown eval error";
82
+ throw new Error(`Reddit eval error: ${msg}`);
83
+ }
84
+ return result.result?.value;
85
+ }
86
+ // ==================== Reddit API Helpers ====================
87
+ async function getModhash() {
88
+ const result = await evalInReddit(`
89
+ (async () => {
90
+ const resp = await fetch('https://www.reddit.com/api/me.json', { credentials: 'include' });
91
+ const data = await resp.json();
92
+ return data.data?.modhash || '';
93
+ })()
94
+ `);
95
+ if (!result) {
96
+ throw new Error("Could not get modhash — you may not be logged in to Reddit. " +
97
+ "Open reddit.com in Chrome and log in.");
98
+ }
99
+ return result;
100
+ }
101
+ async function redditGet(path, params = {}) {
102
+ const qs = new URLSearchParams(params).toString();
103
+ const url = `https://www.reddit.com${path}${qs ? "?" + qs : ""}`;
104
+ const result = await evalInReddit(`
105
+ (async () => {
106
+ const resp = await fetch('${url}', { credentials: 'include' });
107
+ if (resp.status === 429) return { error: 'rate_limited', status: 429 };
108
+ if (!resp.ok) return { error: resp.status, statusText: resp.statusText };
109
+ return await resp.json();
110
+ })()
111
+ `);
112
+ if (result?.error === "rate_limited") {
113
+ throw new Error("Reddit rate limited this request. Wait a few minutes and try again.");
114
+ }
115
+ if (result?.error && typeof result.error === "number") {
116
+ throw new Error(`Reddit API returned HTTP ${result.error}: ${result.statusText}`);
117
+ }
118
+ return result;
119
+ }
120
+ async function redditPost(path, params) {
121
+ await rateLimit();
122
+ const modhash = await getModhash();
123
+ params.uh = modhash;
124
+ params.api_type = "json";
125
+ const body = new URLSearchParams(params).toString();
126
+ const result = await evalInReddit(`
127
+ (async () => {
128
+ const resp = await fetch('https://www.reddit.com${path}', {
129
+ method: 'POST',
130
+ credentials: 'include',
131
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
132
+ body: '${body.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'
133
+ });
134
+ if (resp.status === 429) return { json: { errors: [['RATELIMIT', 'Rate limited by Reddit. Wait a few minutes.']] } };
135
+ const text = await resp.text();
136
+ try { return JSON.parse(text); } catch(e) { return { error: 'parse_error', raw: text.substring(0, 500) }; }
137
+ })()
138
+ `);
139
+ // Check for Reddit API errors
140
+ if (result?.json?.errors?.length) {
141
+ const errors = result.json.errors.map((e) => (Array.isArray(e) ? e.join(": ") : e));
142
+ throw new Error(`Reddit API error: ${errors.join("; ")}`);
143
+ }
144
+ return result;
145
+ }
146
+ function wrapTool(handler) {
147
+ return async (args) => {
148
+ try {
149
+ return await handler(args);
150
+ }
151
+ catch (e) {
152
+ const msg = e.message || String(e);
153
+ console.error(`[reddit-mcp] Error: ${msg}`);
154
+ return {
155
+ content: [{ type: "text", text: `Error: ${msg}` }],
156
+ isError: true,
157
+ };
158
+ }
159
+ };
160
+ }
161
+ // ==================== MCP Tools — Read Operations ====================
162
+ server.tool("reddit_ensure_session", "Check if Chrome is running, Reddit tab exists, and user is logged in. Call this first.", {}, wrapTool(async () => {
163
+ const data = await redditGet("/api/me.json");
164
+ const username = data.data?.name;
165
+ if (username) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Connected. Logged in as u/${username}. Reddit API ready.`,
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: "Reddit tab found but not logged in. Please log in to Reddit in Chrome first.",
180
+ },
181
+ ],
182
+ };
183
+ }));
184
+ server.tool("reddit_me", "Get current logged-in Reddit user info (karma, username, account age, etc.)", {}, wrapTool(async () => {
185
+ const data = await redditGet("/api/me.json");
186
+ return {
187
+ content: [
188
+ { type: "text", text: JSON.stringify(data.data || data, null, 2) },
189
+ ],
190
+ };
191
+ }));
192
+ server.tool("reddit_get_post", "Get a Reddit post and its comments by URL or post ID", {
193
+ url: z
194
+ .string()
195
+ .describe("Reddit post URL or post ID (e.g. '1sdv3lo')"),
196
+ sort: z
197
+ .enum(["top", "new", "best", "controversial", "old", "qa"])
198
+ .default("top")
199
+ .describe("Comment sort order"),
200
+ limit: z.number().default(100).describe("Max comments to return"),
201
+ }, wrapTool(async ({ url, sort, limit }) => {
202
+ const match = url.match(/comments\/([a-z0-9]+)/);
203
+ const postId = match ? match[1] : url;
204
+ const data = await redditGet(`/comments/${postId}.json`, {
205
+ sort,
206
+ limit: String(limit),
207
+ });
208
+ if (Array.isArray(data) && data.length >= 2) {
209
+ const post = data[0]?.data?.children?.[0]?.data;
210
+ const comments = data[1]?.data?.children?.map((c) => c.data) || [];
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: JSON.stringify({ post, comments }, null, 2),
216
+ },
217
+ ],
218
+ };
219
+ }
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
222
+ };
223
+ }));
224
+ server.tool("reddit_get_comments", "Get all comments for a post as a flat list with author, body, score, and parent info", {
225
+ url: z.string().describe("Reddit post URL or post ID"),
226
+ sort: z
227
+ .enum(["top", "new", "best", "controversial", "old", "qa"])
228
+ .default("top"),
229
+ }, wrapTool(async ({ url, sort }) => {
230
+ const match = url.match(/comments\/([a-z0-9]+)/);
231
+ const postId = match ? match[1] : url;
232
+ const data = await redditGet(`/comments/${postId}.json`, {
233
+ sort,
234
+ limit: "500",
235
+ });
236
+ if (!Array.isArray(data) || data.length < 2) {
237
+ return {
238
+ content: [{ type: "text", text: "Could not fetch comments" }],
239
+ };
240
+ }
241
+ const flat = [];
242
+ function extract(children, depth = 0) {
243
+ for (const child of children) {
244
+ if (child.kind !== "t1")
245
+ continue;
246
+ const c = child.data;
247
+ flat.push({
248
+ id: c.name,
249
+ author: c.author,
250
+ body: c.body,
251
+ score: c.score,
252
+ parent_id: c.parent_id,
253
+ depth,
254
+ created_utc: c.created_utc,
255
+ });
256
+ if (c.replies?.data?.children) {
257
+ extract(c.replies.data.children, depth + 1);
258
+ }
259
+ }
260
+ }
261
+ extract(data[1].data.children);
262
+ return {
263
+ content: [{ type: "text", text: JSON.stringify(flat, null, 2) }],
264
+ };
265
+ }));
266
+ server.tool("reddit_get_user", "Get a Reddit user's profile, posts, or comments", {
267
+ username: z.string().describe("Reddit username"),
268
+ type: z
269
+ .enum(["about", "overview", "submitted", "comments"])
270
+ .default("about"),
271
+ limit: z.number().default(25),
272
+ }, wrapTool(async ({ username, type, limit }) => {
273
+ const path = type === "about"
274
+ ? `/user/${username}/about.json`
275
+ : `/user/${username}/${type}.json`;
276
+ const data = await redditGet(path, { limit: String(limit) });
277
+ return {
278
+ content: [
279
+ { type: "text", text: JSON.stringify(data.data || data, null, 2) },
280
+ ],
281
+ };
282
+ }));
283
+ server.tool("reddit_get_subreddit", "Get subreddit info, rules, or browse posts (hot/new/top/rising)", {
284
+ subreddit: z.string().describe("Subreddit name (without r/)"),
285
+ type: z
286
+ .enum(["about", "rules", "hot", "new", "top", "rising"])
287
+ .default("hot"),
288
+ limit: z.number().default(25),
289
+ time: z
290
+ .enum(["hour", "day", "week", "month", "year", "all"])
291
+ .default("day")
292
+ .describe("Time filter for top/controversial"),
293
+ }, wrapTool(async ({ subreddit, type, limit, time }) => {
294
+ const path = ["about", "rules"].includes(type)
295
+ ? `/r/${subreddit}/about/${type === "about" ? "" : "rules"}.json`
296
+ : `/r/${subreddit}/${type}.json`;
297
+ const data = await redditGet(path, { limit: String(limit), t: time });
298
+ return {
299
+ content: [
300
+ { type: "text", text: JSON.stringify(data.data || data, null, 2) },
301
+ ],
302
+ };
303
+ }));
304
+ server.tool("reddit_search", "Search Reddit posts globally or within a specific subreddit", {
305
+ query: z.string().describe("Search query"),
306
+ subreddit: z
307
+ .string()
308
+ .optional()
309
+ .describe("Limit search to this subreddit (optional)"),
310
+ sort: z
311
+ .enum(["relevance", "hot", "top", "new", "comments"])
312
+ .default("relevance"),
313
+ time: z
314
+ .enum(["hour", "day", "week", "month", "year", "all"])
315
+ .default("all"),
316
+ limit: z.number().default(25),
317
+ }, wrapTool(async ({ query, subreddit, sort, time, limit }) => {
318
+ const path = subreddit ? `/r/${subreddit}/search.json` : `/search.json`;
319
+ const params = {
320
+ q: query,
321
+ sort,
322
+ t: time,
323
+ limit: String(limit),
324
+ };
325
+ if (subreddit)
326
+ params.restrict_sr = "on";
327
+ const data = await redditGet(path, params);
328
+ return {
329
+ content: [
330
+ { type: "text", text: JSON.stringify(data.data || data, null, 2) },
331
+ ],
332
+ };
333
+ }));
334
+ server.tool("reddit_get_inbox", "Get Reddit messages (inbox, unread, or sent)", {
335
+ type: z.enum(["inbox", "unread", "sent"]).default("unread"),
336
+ limit: z.number().default(25),
337
+ }, wrapTool(async ({ type, limit }) => {
338
+ const data = await redditGet(`/message/${type}.json`, {
339
+ limit: String(limit),
340
+ });
341
+ return {
342
+ content: [
343
+ { type: "text", text: JSON.stringify(data.data || data, null, 2) },
344
+ ],
345
+ };
346
+ }));
347
+ server.tool("reddit_get_subscriptions", "Get your subscribed subreddits", {
348
+ limit: z.number().default(100).describe("Max subreddits to return"),
349
+ }, wrapTool(async ({ limit }) => {
350
+ const data = await redditGet("/subreddits/mine/subscriber.json", {
351
+ limit: String(limit),
352
+ });
353
+ const subs = data.data?.children?.map((c) => ({
354
+ name: c.data.display_name,
355
+ title: c.data.title,
356
+ subscribers: c.data.subscribers,
357
+ url: c.data.url,
358
+ })) || [];
359
+ return {
360
+ content: [{ type: "text", text: JSON.stringify(subs, null, 2) }],
361
+ };
362
+ }));
363
+ // ==================== MCP Tools — Write Operations ====================
364
+ server.tool("reddit_comment", "Reply to a Reddit post or comment. thing_id must start with t1_ (comment) or t3_ (post).", {
365
+ thing_id: z
366
+ .string()
367
+ .describe("Fullname of parent (t1_ for comment, t3_ for post)"),
368
+ text: z.string().describe("Comment text (markdown supported)"),
369
+ }, wrapTool(async ({ thing_id, text }) => {
370
+ const result = await redditPost("/api/comment", { thing_id, text });
371
+ const commentData = result.json?.data?.things?.[0]?.data;
372
+ const commentId = commentData?.id || commentData?.name;
373
+ return {
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: JSON.stringify({
378
+ success: true,
379
+ comment_id: commentId,
380
+ }),
381
+ },
382
+ ],
383
+ };
384
+ }));
385
+ server.tool("reddit_submit", "Create a new Reddit post (text or link)", {
386
+ subreddit: z.string().describe("Subreddit name (without r/)"),
387
+ title: z.string().describe("Post title"),
388
+ kind: z.enum(["self", "link"]).describe("Post type: self (text) or link"),
389
+ text: z.string().optional().describe("Post body text (for self posts)"),
390
+ url: z.string().optional().describe("URL (for link posts)"),
391
+ flair_id: z.string().optional().describe("Flair template ID"),
392
+ flair_text: z.string().optional().describe("Flair text"),
393
+ nsfw: z.boolean().default(false),
394
+ spoiler: z.boolean().default(false),
395
+ }, wrapTool(async ({ subreddit, title, kind, text, url, flair_id, flair_text, nsfw, spoiler, }) => {
396
+ const params = { sr: subreddit, title, kind };
397
+ if (text)
398
+ params.text = text;
399
+ if (url)
400
+ params.url = url;
401
+ if (flair_id)
402
+ params.flair_id = flair_id;
403
+ if (flair_text)
404
+ params.flair_text = flair_text;
405
+ if (nsfw)
406
+ params.nsfw = "true";
407
+ if (spoiler)
408
+ params.spoiler = "true";
409
+ params.sendreplies = "true";
410
+ params.resubmit = "true";
411
+ const result = await redditPost("/api/submit", params);
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: JSON.stringify(result.json || result, null, 2),
417
+ },
418
+ ],
419
+ };
420
+ }));
421
+ server.tool("reddit_vote", "Upvote, downvote, or unvote on a post or comment", {
422
+ thing_id: z.string().describe("Fullname (t1_ or t3_)"),
423
+ direction: z.enum(["up", "down", "unvote"]).describe("Vote direction"),
424
+ }, wrapTool(async ({ thing_id, direction }) => {
425
+ const dir = direction === "up" ? "1" : direction === "down" ? "-1" : "0";
426
+ await redditPost("/api/vote", { id: thing_id, dir });
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: JSON.stringify({ success: true, thing_id, direction }),
432
+ },
433
+ ],
434
+ };
435
+ }));
436
+ server.tool("reddit_save", "Save or unsave a Reddit post or comment", {
437
+ thing_id: z.string().describe("Fullname (t1_ or t3_)"),
438
+ unsave: z.boolean().default(false).describe("Set true to unsave"),
439
+ }, wrapTool(async ({ thing_id, unsave }) => {
440
+ const path = unsave ? "/api/unsave" : "/api/save";
441
+ await redditPost(path, { id: thing_id });
442
+ return {
443
+ content: [
444
+ {
445
+ type: "text",
446
+ text: JSON.stringify({
447
+ success: true,
448
+ action: unsave ? "unsaved" : "saved",
449
+ thing_id,
450
+ }),
451
+ },
452
+ ],
453
+ };
454
+ }));
455
+ server.tool("reddit_send_message", "Send a Reddit private message to a user", {
456
+ to: z.string().describe("Username to send to"),
457
+ subject: z.string().describe("Message subject"),
458
+ text: z.string().describe("Message body"),
459
+ }, wrapTool(async ({ to, subject, text }) => {
460
+ await redditPost("/api/compose", { to, subject, text });
461
+ return {
462
+ content: [
463
+ {
464
+ type: "text",
465
+ text: JSON.stringify({ success: true, to, subject }),
466
+ },
467
+ ],
468
+ };
469
+ }));
470
+ server.tool("reddit_edit", "Edit your own post or comment", {
471
+ thing_id: z
472
+ .string()
473
+ .describe("Fullname of your post/comment to edit"),
474
+ text: z.string().describe("New text content"),
475
+ }, wrapTool(async ({ thing_id, text }) => {
476
+ await redditPost("/api/editusertext", { thing_id, text });
477
+ return {
478
+ content: [
479
+ {
480
+ type: "text",
481
+ text: JSON.stringify({ success: true, edited: thing_id }),
482
+ },
483
+ ],
484
+ };
485
+ }));
486
+ server.tool("reddit_delete", "Delete your own post or comment", {
487
+ thing_id: z
488
+ .string()
489
+ .describe("Fullname of your post/comment to delete"),
490
+ }, wrapTool(async ({ thing_id }) => {
491
+ await redditPost("/api/del", { id: thing_id });
492
+ return {
493
+ content: [
494
+ {
495
+ type: "text",
496
+ text: JSON.stringify({ success: true, deleted: thing_id }),
497
+ },
498
+ ],
499
+ };
500
+ }));
501
+ server.tool("reddit_subscribe", "Subscribe or unsubscribe to a subreddit", {
502
+ subreddit: z.string().describe("Subreddit name (without r/)"),
503
+ unsubscribe: z.boolean().default(false),
504
+ }, wrapTool(async ({ subreddit, unsubscribe }) => {
505
+ const action = unsubscribe ? "unsub" : "sub";
506
+ await redditPost("/api/subscribe", { action, sr_name: subreddit });
507
+ return {
508
+ content: [
509
+ {
510
+ type: "text",
511
+ text: JSON.stringify({ success: true, action, subreddit }),
512
+ },
513
+ ],
514
+ };
515
+ }));
516
+ // ==================== Start Server ====================
517
+ async function main() {
518
+ const transport = new StdioServerTransport();
519
+ await server.connect(transport);
520
+ console.error("reddit-unofficial-api MCP server running on stdio");
521
+ }
522
+ main().catch((e) => {
523
+ console.error("Fatal:", e);
524
+ process.exit(1);
525
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mcpware/reddit-unofficial-api",
3
+ "version": "0.1.0",
4
+ "description": "Free Reddit API through Chrome DevTools. No API keys needed, just a logged-in browser.",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "reddit-unofficial-api": "build/index.js"
9
+ },
10
+ "files": [
11
+ "build",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/index.ts",
18
+ "start": "node build/index.js",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/mcpware/reddit-unofficial-api.git"
24
+ },
25
+ "homepage": "https://github.com/mcpware/reddit-unofficial-api",
26
+ "bugs": {
27
+ "url": "https://github.com/mcpware/reddit-unofficial-api/issues"
28
+ },
29
+ "author": "mcpware",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.1",
33
+ "chrome-remote-interface": "^0.34.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/chrome-remote-interface": "^0.33.0",
37
+ "@types/node": "^22.15.3",
38
+ "tsx": "^4.19.4",
39
+ "typescript": "^5.8.3"
40
+ },
41
+ "keywords": [
42
+ "reddit",
43
+ "api",
44
+ "mcp",
45
+ "chrome-devtools",
46
+ "cdp",
47
+ "unofficial",
48
+ "free",
49
+ "model-context-protocol"
50
+ ]
51
+ }