reddit-mcp-server 1.0.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.
@@ -0,0 +1,74 @@
1
+ const eslint = require('@eslint/js');
2
+ const tseslint = require('@typescript-eslint/eslint-plugin');
3
+ const tsParser = require('@typescript-eslint/parser');
4
+ const prettierConfig = require('eslint-config-prettier');
5
+ const prettierPlugin = require('eslint-plugin-prettier');
6
+
7
+ module.exports = [
8
+ {
9
+ ...eslint.configs.recommended,
10
+ languageOptions: {
11
+ globals: {
12
+ console: 'readonly',
13
+ process: 'readonly',
14
+ Buffer: 'readonly',
15
+ __dirname: 'readonly',
16
+ __filename: 'readonly',
17
+ exports: 'writable',
18
+ module: 'writable',
19
+ require: 'readonly',
20
+ global: 'readonly',
21
+ URL: 'readonly',
22
+ },
23
+ },
24
+ },
25
+ prettierConfig,
26
+ {
27
+ files: ['**/*.{ts,tsx}'],
28
+ languageOptions: {
29
+ parser: tsParser,
30
+ parserOptions: {
31
+ ecmaVersion: 2022,
32
+ sourceType: 'module',
33
+ project: './tsconfig.json',
34
+ },
35
+ globals: {
36
+ console: 'readonly',
37
+ process: 'readonly',
38
+ Buffer: 'readonly',
39
+ __dirname: 'readonly',
40
+ __filename: 'readonly',
41
+ exports: 'writable',
42
+ module: 'writable',
43
+ require: 'readonly',
44
+ global: 'readonly',
45
+ URL: 'readonly',
46
+ },
47
+ },
48
+ plugins: {
49
+ '@typescript-eslint': tseslint,
50
+ prettier: prettierPlugin,
51
+ },
52
+ rules: {
53
+ ...tseslint.configs.recommended.rules,
54
+ 'prettier/prettier': 'error',
55
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
56
+ '@typescript-eslint/no-explicit-any': 'warn',
57
+ '@typescript-eslint/explicit-function-return-type': 'off',
58
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
59
+ '@typescript-eslint/no-non-null-assertion': 'warn',
60
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
61
+ '@typescript-eslint/no-unsafe-member-access': 'warn',
62
+ '@typescript-eslint/no-unsafe-argument': 'warn',
63
+ '@typescript-eslint/no-unsafe-return': 'warn',
64
+ '@typescript-eslint/no-unsafe-call': 'warn',
65
+ '@typescript-eslint/restrict-template-expressions': 'warn',
66
+ '@typescript-eslint/prefer-promise-reject-errors': 'warn',
67
+ 'no-console': 'off',
68
+ 'no-regex-spaces': 'off',
69
+ },
70
+ },
71
+ {
72
+ ignores: ['node_modules/**', 'dist/**', 'build/**', '*.config.js', '*.config.ts'],
73
+ },
74
+ ];
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "reddit-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol (MCP) that provides tools for fetching and creating Reddit content. Fork of the alexandros-lekkas/reddit-mcp-server.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "reddit-mcp-server": "dist/index.js"
8
+ },
9
+ "keywords": [
10
+ "mcp",
11
+ "reddit",
12
+ "api",
13
+ "model-context-protocol"
14
+ ],
15
+ "author": "Jordan Burke <jordan.burke@gmail.com>",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.13.1",
19
+ "axios": "^1.10.0",
20
+ "dotenv": "^16.5.0"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.29.0",
24
+ "@types/node": "^22.15.33",
25
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
26
+ "@typescript-eslint/parser": "^8.35.0",
27
+ "eslint": "^9.29.0",
28
+ "eslint-config-prettier": "^10.1.5",
29
+ "eslint-plugin-prettier": "^5.5.1",
30
+ "prettier": "^3.6.1",
31
+ "tsup": "^8.5.0",
32
+ "typescript": "^5.8.3"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "inspect": "pnpm build && npx @modelcontextprotocol/inspector dist/index.js",
37
+ "dev": "pnpm build && pnpm inspect",
38
+ "start": "pnpm build && npx reddit-mcp-server",
39
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
40
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
41
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
42
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix"
43
+ }
44
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - '.'
package/smithery.yaml ADDED
@@ -0,0 +1,37 @@
1
+ # Smithery configuration file: https://smithery.ai/docs/build/project-config
2
+
3
+ startCommand:
4
+ type: stdio
5
+ commandFunction:
6
+ # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
7
+ |-
8
+ (config) => ({ command: 'node', args: ['build/index.js'], env: { REDDIT_CLIENT_ID: config.redditClientId, REDDIT_CLIENT_SECRET: config.redditClientSecret, REDDIT_USER_AGENT: config.redditUserAgent ?? 'RedditMCPServer/0.1.0', ...(config.redditUsername ? { REDDIT_USERNAME: config.redditUsername } : {}), ...(config.redditPassword ? { REDDIT_PASSWORD: config.redditPassword } : {}) } })
9
+ configSchema:
10
+ # JSON Schema defining the configuration options for the MCP.
11
+ type: object
12
+ required:
13
+ - redditClientId
14
+ - redditClientSecret
15
+ properties:
16
+ redditClientId:
17
+ type: string
18
+ description: Reddit Application Client ID
19
+ redditClientSecret:
20
+ type: string
21
+ description: Reddit Application Client Secret
22
+ redditUserAgent:
23
+ type: string
24
+ default: RedditMCPServer/0.1.0
25
+ description: User-Agent header for Reddit API requests
26
+ redditUsername:
27
+ type: string
28
+ description: Optional Reddit username for authenticated actions
29
+ redditPassword:
30
+ type: string
31
+ description: Optional Reddit password for authenticated actions
32
+ exampleConfig:
33
+ redditClientId: abc123xyz
34
+ redditClientSecret: secretvalue
35
+ redditUserAgent: MyRedditAgent/1.0.0
36
+ redditUsername: my_reddit_user
37
+ redditPassword: supersecret
@@ -0,0 +1,340 @@
1
+ import axios, { AxiosInstance } from "axios"
2
+ import { RedditClientConfig, RedditUser, RedditPost, RedditComment, RedditSubreddit } from "../types"
3
+
4
+ export class RedditClient {
5
+ private clientId: string
6
+ private clientSecret: string
7
+ private userAgent: string
8
+ private username?: string
9
+ private password?: string
10
+ private accessToken?: string
11
+ private tokenExpiry: number = 0
12
+ private api: AxiosInstance
13
+ private authenticated: boolean = false
14
+
15
+ constructor(config: RedditClientConfig) {
16
+ this.clientId = config.clientId
17
+ this.clientSecret = config.clientSecret
18
+ this.userAgent = config.userAgent
19
+ this.username = config.username
20
+ this.password = config.password
21
+
22
+ this.api = axios.create({
23
+ baseURL: "https://oauth.reddit.com",
24
+ headers: {
25
+ "User-Agent": this.userAgent,
26
+ },
27
+ })
28
+
29
+ // Add response interceptor to handle token refresh
30
+ this.api.interceptors.response.use(
31
+ (response) => response,
32
+ async (error) => {
33
+ if (error.response?.status === 401 && this.authenticated) {
34
+ await this.authenticate()
35
+ const originalRequest = error.config
36
+ originalRequest.headers["Authorization"] = `Bearer ${this.accessToken}`
37
+ return this.api(originalRequest)
38
+ }
39
+ return Promise.reject(error)
40
+ },
41
+ )
42
+ }
43
+
44
+ async authenticate(): Promise<void> {
45
+ try {
46
+ const now = Date.now()
47
+ if (this.accessToken && now < this.tokenExpiry) {
48
+ return
49
+ }
50
+
51
+ const authUrl = "https://www.reddit.com/api/v1/access_token"
52
+ const authData = new URLSearchParams()
53
+
54
+ if (this.username && this.password) {
55
+ console.log(`[Auth] Authenticating with user credentials for ${this.username}`)
56
+ authData.append("grant_type", "password")
57
+ authData.append("username", this.username)
58
+ authData.append("password", this.password)
59
+ } else {
60
+ console.log("[Auth] Authenticating with client credentials (read-only)")
61
+ authData.append("grant_type", "client_credentials")
62
+ }
63
+
64
+ const response = await axios.post(authUrl, authData, {
65
+ auth: {
66
+ username: this.clientId,
67
+ password: this.clientSecret,
68
+ },
69
+ headers: {
70
+ "User-Agent": this.userAgent,
71
+ "Content-Type": "application/x-www-form-urlencoded",
72
+ },
73
+ })
74
+
75
+ this.accessToken = response.data.access_token
76
+ this.tokenExpiry = now + response.data.expires_in * 1000
77
+ this.authenticated = true
78
+ this.api.defaults.headers.common["Authorization"] = `Bearer ${this.accessToken}`
79
+
80
+ console.log("[Auth] Successfully authenticated with Reddit API")
81
+ } catch (error) {
82
+ console.error("[Auth] Authentication error:", error)
83
+ throw new Error("Failed to authenticate with Reddit API")
84
+ }
85
+ }
86
+
87
+ async checkAuthentication(): Promise<boolean> {
88
+ if (!this.authenticated) {
89
+ try {
90
+ await this.authenticate()
91
+ return true
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+ return true
97
+ }
98
+
99
+ async getUser(username: string): Promise<RedditUser> {
100
+ await this.authenticate()
101
+ try {
102
+ const response = await this.api.get(`/user/${username}/about.json`)
103
+ const data = response.data.data
104
+
105
+ return {
106
+ name: data.name,
107
+ id: data.id,
108
+ commentKarma: data.comment_karma,
109
+ linkKarma: data.link_karma,
110
+ totalKarma: data.total_karma || data.comment_karma + data.link_karma,
111
+ isMod: data.is_mod,
112
+ isGold: data.is_gold,
113
+ isEmployee: data.is_employee,
114
+ createdUtc: data.created_utc,
115
+ profileUrl: `https://reddit.com/user/${data.name}`,
116
+ }
117
+ } catch (error) {
118
+ console.error(`[Error] Failed to get user info for ${username}:`, error)
119
+ throw new Error(`Failed to get user info for ${username}`)
120
+ }
121
+ }
122
+
123
+ async getSubredditInfo(subredditName: string): Promise<RedditSubreddit> {
124
+ await this.authenticate()
125
+ try {
126
+ const response = await this.api.get(`/r/${subredditName}/about.json`)
127
+ const data = response.data.data
128
+
129
+ return {
130
+ displayName: data.display_name,
131
+ title: data.title,
132
+ description: data.description || "",
133
+ publicDescription: data.public_description || "",
134
+ subscribers: data.subscribers,
135
+ activeUserCount: data.active_user_count,
136
+ createdUtc: data.created_utc,
137
+ over18: data.over18,
138
+ subredditType: data.subreddit_type,
139
+ url: data.url,
140
+ }
141
+ } catch (error) {
142
+ console.error(`[Error] Failed to get subreddit info for ${subredditName}:`, error)
143
+ throw new Error(`Failed to get subreddit info for ${subredditName}`)
144
+ }
145
+ }
146
+
147
+ async getTopPosts(subreddit: string, timeFilter: string = "week", limit: number = 10): Promise<RedditPost[]> {
148
+ await this.authenticate()
149
+ try {
150
+ const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json"
151
+ const response = await this.api.get(endpoint, {
152
+ params: {
153
+ t: timeFilter,
154
+ limit,
155
+ },
156
+ })
157
+
158
+ return response.data.data.children.map((child: any) => {
159
+ const post = child.data
160
+ return {
161
+ id: post.id,
162
+ title: post.title,
163
+ author: post.author,
164
+ subreddit: post.subreddit,
165
+ selftext: post.selftext,
166
+ url: post.url,
167
+ score: post.score,
168
+ upvoteRatio: post.upvote_ratio,
169
+ numComments: post.num_comments,
170
+ createdUtc: post.created_utc,
171
+ over18: post.over_18,
172
+ spoiler: post.spoiler,
173
+ edited: !!post.edited,
174
+ isSelf: post.is_self,
175
+ linkFlairText: post.link_flair_text,
176
+ permalink: post.permalink,
177
+ }
178
+ })
179
+ } catch (error) {
180
+ console.error(`[Error] Failed to get top posts for ${subreddit || "home"}:`, error)
181
+ throw new Error(`Failed to get top posts for ${subreddit || "home"}`)
182
+ }
183
+ }
184
+
185
+ async getPost(postId: string, subreddit?: string): Promise<RedditPost> {
186
+ await this.authenticate()
187
+ try {
188
+ const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`
189
+
190
+ const response = await this.api.get(endpoint)
191
+
192
+ let post
193
+ if (subreddit) {
194
+ // When using the comments endpoint
195
+ post = response.data[0].data.children[0].data
196
+ } else {
197
+ // When using the info endpoint
198
+ if (!response.data.data.children.length) {
199
+ throw new Error(`Post with ID ${postId} not found`)
200
+ }
201
+ post = response.data.data.children[0].data
202
+ }
203
+
204
+ return {
205
+ id: post.id,
206
+ title: post.title,
207
+ author: post.author,
208
+ subreddit: post.subreddit,
209
+ selftext: post.selftext,
210
+ url: post.url,
211
+ score: post.score,
212
+ upvoteRatio: post.upvote_ratio,
213
+ numComments: post.num_comments,
214
+ createdUtc: post.created_utc,
215
+ over18: post.over_18,
216
+ spoiler: post.spoiler,
217
+ edited: !!post.edited,
218
+ isSelf: post.is_self,
219
+ linkFlairText: post.link_flair_text,
220
+ permalink: post.permalink,
221
+ }
222
+ } catch (error) {
223
+ console.error(`[Error] Failed to get post with ID ${postId}:`, error)
224
+ throw new Error(`Failed to get post with ID ${postId}`)
225
+ }
226
+ }
227
+
228
+ async getTrendingSubreddits(limit: number = 5): Promise<string[]> {
229
+ await this.authenticate()
230
+ try {
231
+ const response = await this.api.get("/subreddits/popular.json", {
232
+ params: { limit },
233
+ })
234
+
235
+ return response.data.data.children.map((child: any) => child.data.display_name)
236
+ } catch (error) {
237
+ console.error("[Error] Failed to get trending subreddits:", error)
238
+ throw new Error("Failed to get trending subreddits")
239
+ }
240
+ }
241
+
242
+ async createPost(subreddit: string, title: string, content: string, isSelf: boolean = true): Promise<RedditPost> {
243
+ await this.authenticate()
244
+
245
+ if (!this.username || !this.password) {
246
+ throw new Error("User authentication required for posting")
247
+ }
248
+
249
+ try {
250
+ const kind = isSelf ? "self" : "link"
251
+ const params = new URLSearchParams()
252
+ params.append("sr", subreddit)
253
+ params.append("kind", kind)
254
+ params.append("title", title)
255
+ params.append(isSelf ? "text" : "url", content)
256
+
257
+ const response = await this.api.post("/api/submit", params, {
258
+ headers: {
259
+ "Content-Type": "application/x-www-form-urlencoded",
260
+ },
261
+ })
262
+
263
+ if (response.data.success) {
264
+ // Get the newly created post
265
+ const postId = response.data.data.id
266
+ return await this.getPost(postId)
267
+ } else {
268
+ throw new Error("Failed to create post")
269
+ }
270
+ } catch (error) {
271
+ console.error(`[Error] Failed to create post in ${subreddit}:`, error)
272
+ throw new Error(`Failed to create post in ${subreddit}`)
273
+ }
274
+ }
275
+
276
+ async checkPostExists(postId: string): Promise<boolean> {
277
+ await this.authenticate()
278
+ try {
279
+ const response = await this.api.get(`/api/info.json?id=t3_${postId}`)
280
+ return response.data.data.children.length > 0
281
+ } catch {
282
+ return false
283
+ }
284
+ }
285
+
286
+ async replyToPost(postId: string, content: string): Promise<RedditComment> {
287
+ await this.authenticate()
288
+
289
+ if (!this.username || !this.password) {
290
+ throw new Error("User authentication required for posting replies")
291
+ }
292
+
293
+ try {
294
+ if (!(await this.checkPostExists(postId))) {
295
+ throw new Error(`Post with ID ${postId} does not exist or is not accessible`)
296
+ }
297
+
298
+ const params = new URLSearchParams()
299
+ params.append("thing_id", `t3_${postId}`)
300
+ params.append("text", content)
301
+
302
+ const response = await this.api.post("/api/comment", params, {
303
+ headers: {
304
+ "Content-Type": "application/x-www-form-urlencoded",
305
+ },
306
+ })
307
+
308
+ // Extract comment data from response
309
+ const commentData = response.data
310
+ return {
311
+ id: commentData.id,
312
+ author: this.username,
313
+ body: content,
314
+ score: 1,
315
+ controversiality: 0,
316
+ subreddit: commentData.subreddit,
317
+ submissionTitle: commentData.link_title,
318
+ createdUtc: Date.now() / 1000,
319
+ edited: false,
320
+ isSubmitter: false,
321
+ permalink: commentData.permalink,
322
+ }
323
+ } catch (error) {
324
+ console.error(`[Error] Failed to reply to post ${postId}:`, error)
325
+ throw new Error(`Failed to reply to post ${postId}`)
326
+ }
327
+ }
328
+ }
329
+
330
+ // Create and export singleton instance
331
+ let redditClient: RedditClient | null = null
332
+
333
+ export function initializeRedditClient(config: RedditClientConfig): RedditClient {
334
+ redditClient = new RedditClient(config)
335
+ return redditClient
336
+ }
337
+
338
+ export function getRedditClient(): RedditClient | null {
339
+ return redditClient
340
+ }