Skip to content

Commit 2eb4dc6

Browse files
Feat/reddit (#86)
* Add Reddit MCP module with tools for subreddit interaction - Introduced a new `reddit` module, including package.json, README, and TypeScript configuration. - Implemented tools for fetching posts from subreddits and searching Reddit content. - Updated bun.lock and package.json to include new dependencies for the Reddit module. - Created server structure and types for handling API requests and responses. - Added comprehensive documentation in README for usage and development instructions. * Update input schemas to use z.coerce for limit validation in subreddit and search requests
1 parent d341db9 commit 2eb4dc6

File tree

13 files changed

+807
-26
lines changed

13 files changed

+807
-26
lines changed

bun.lock

Lines changed: 71 additions & 26 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"perplexity",
3333
"pinecone",
3434
"readonly-sql",
35+
"reddit",
3536
"registry",
3637
"replicate",
3738
"shared",

reddit/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Reddit MCP
2+
3+
MCP server para interagir com o Reddit. Permite buscar posts de subreddits e pesquisar conteúdo.
4+
5+
## Tools Disponíveis
6+
7+
### GET_SUBREDDIT_POSTS
8+
9+
Busca posts de um subreddit específico.
10+
11+
**Parâmetros:**
12+
- `subreddit` (obrigatório): Nome do subreddit (sem o "r/"). Ex: "mcp", "programming", "news"
13+
- `sort` (opcional): Como ordenar os posts - "hot", "new", "top", "rising" (padrão: "hot")
14+
- `time` (opcional): Filtro de tempo para ordenação "top" - "hour", "day", "week", "month", "year", "all"
15+
- `limit` (opcional): Número de posts a retornar (1-100, padrão: 25)
16+
- `after` (opcional): Cursor para paginação
17+
18+
**Exemplo de uso:**
19+
```
20+
Busque os posts mais recentes do r/mcp
21+
```
22+
23+
### SEARCH_REDDIT
24+
25+
Pesquisa posts no Reddit por termo de busca.
26+
27+
**Parâmetros:**
28+
- `query` (obrigatório): Termo de busca
29+
- `subreddit` (opcional): Limitar busca a um subreddit específico
30+
- `sort` (opcional): Como ordenar - "relevance", "hot", "top", "new", "comments" (padrão: "relevance")
31+
- `time` (opcional): Filtro de tempo - "hour", "day", "week", "month", "year", "all" (padrão: "all")
32+
- `limit` (opcional): Número de resultados (1-100, padrão: 25)
33+
- `after` (opcional): Cursor para paginação
34+
35+
**Exemplo de uso:**
36+
```
37+
Pesquise por "MCP server" no Reddit
38+
Busque posts sobre "AI agents" no r/LocalLLaMA
39+
```
40+
41+
## Instalação
42+
43+
Este MCP não requer configuração adicional - utiliza a API pública do Reddit que não requer autenticação.
44+
45+
## Desenvolvimento
46+
47+
```bash
48+
# Instalar dependências
49+
bun install
50+
51+
# Rodar em desenvolvimento
52+
bun run dev
53+
54+
# Verificar tipos
55+
bun run check
56+
57+
# Deploy
58+
bun run deploy
59+
```
60+
61+

reddit/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "reddit",
3+
"version": "1.0.0",
4+
"description": "MCP server for Reddit - search subreddits and browse posts",
5+
"private": true,
6+
"type": "module",
7+
"scripts": {
8+
"dev": "deco dev --vite",
9+
"configure": "deco configure",
10+
"gen": "deco gen --output=shared/deco.gen.ts",
11+
"deploy": "npm run build && deco deploy ./dist/server",
12+
"check": "tsc --noEmit",
13+
"build": "bun --bun vite build"
14+
},
15+
"dependencies": {
16+
"@decocms/runtime": "0.25.1",
17+
"zod": "^3.24.3"
18+
},
19+
"devDependencies": {
20+
"@cloudflare/vite-plugin": "^1.13.4",
21+
"@cloudflare/workers-types": "^4.20251014.0",
22+
"@decocms/mcps-shared": "1.0.0",
23+
"@mastra/core": "^0.24.0",
24+
"@modelcontextprotocol/sdk": "^1.21.0",
25+
"@types/mime-db": "^1.43.6",
26+
"deco-cli": "^0.26.0",
27+
"typescript": "^5.7.2",
28+
"vite": "7.2.0",
29+
"wrangler": "^4.28.0"
30+
},
31+
"engines": {
32+
"node": ">=22.0.0"
33+
}
34+
}
35+
36+

reddit/server/lib/types.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { z } from "zod";
2+
3+
/**
4+
* Reddit Post data structure
5+
*/
6+
export interface RedditPost {
7+
id: string;
8+
title: string;
9+
author: string;
10+
subreddit: string;
11+
subreddit_name_prefixed: string;
12+
selftext: string;
13+
url: string;
14+
permalink: string;
15+
score: number;
16+
upvote_ratio: number;
17+
num_comments: number;
18+
created_utc: number;
19+
is_self: boolean;
20+
is_video: boolean;
21+
thumbnail: string;
22+
link_flair_text: string | null;
23+
over_18: boolean;
24+
spoiler: boolean;
25+
stickied: boolean;
26+
}
27+
28+
/**
29+
* Reddit API listing response structure
30+
*/
31+
export interface RedditListingResponse {
32+
kind: string;
33+
data: {
34+
after: string | null;
35+
before: string | null;
36+
dist: number;
37+
modhash: string;
38+
geo_filter: string;
39+
children: Array<{
40+
kind: string;
41+
data: RedditPost;
42+
}>;
43+
};
44+
}
45+
46+
/**
47+
* Sort options for subreddit posts
48+
*/
49+
export const sortOptions = ["hot", "new", "top", "rising"] as const;
50+
export type SortOption = (typeof sortOptions)[number];
51+
52+
/**
53+
* Time filter options for top posts
54+
*/
55+
export const timeFilterOptions = [
56+
"hour",
57+
"day",
58+
"week",
59+
"month",
60+
"year",
61+
"all",
62+
] as const;
63+
export type TimeFilterOption = (typeof timeFilterOptions)[number];
64+
65+
/**
66+
* GET_SUBREDDIT_POSTS input schema
67+
*/
68+
export const getSubredditPostsInputSchema = z.object({
69+
subreddit: z
70+
.string()
71+
.describe(
72+
"Name of the subreddit to fetch posts from (without the 'r/' prefix). Example: 'mcp', 'programming', 'news'",
73+
),
74+
sort: z
75+
.enum(sortOptions)
76+
.optional()
77+
.default("hot")
78+
.describe("How to sort the posts: hot, new, top, or rising"),
79+
time: z
80+
.enum(timeFilterOptions)
81+
.optional()
82+
.describe(
83+
"Time filter for 'top' sort: hour, day, week, month, year, all. Only used when sort is 'top'",
84+
),
85+
limit: z.coerce
86+
.number()
87+
.min(1)
88+
.max(100)
89+
.optional()
90+
.default(25)
91+
.describe("Number of posts to return (1-100, default: 25)"),
92+
after: z
93+
.string()
94+
.optional()
95+
.describe("Fullname of a post to fetch posts after (for pagination)"),
96+
});
97+
98+
/**
99+
* GET_SUBREDDIT_POSTS output schema
100+
*/
101+
export const getSubredditPostsOutputSchema = z.object({
102+
subreddit: z.string().describe("The subreddit name"),
103+
sort: z.string().describe("The sort order used"),
104+
count: z.number().describe("Number of posts returned"),
105+
after: z.string().nullable().describe("Pagination cursor for next page"),
106+
posts: z
107+
.array(
108+
z.object({
109+
id: z.string(),
110+
title: z.string(),
111+
author: z.string(),
112+
selftext: z.string().describe("Post body text (empty if link post)"),
113+
url: z.string().describe("URL of the post or linked content"),
114+
permalink: z.string().describe("Reddit permalink to the post"),
115+
score: z.number().describe("Upvotes minus downvotes"),
116+
num_comments: z.number(),
117+
created_utc: z.number().describe("Unix timestamp of creation"),
118+
is_self: z.boolean().describe("True if text post, false if link post"),
119+
flair: z.string().nullable().describe("Post flair text"),
120+
nsfw: z.boolean().describe("True if marked NSFW"),
121+
}),
122+
)
123+
.describe("List of posts"),
124+
});
125+
126+
/**
127+
* SEARCH_REDDIT input schema
128+
*/
129+
export const searchRedditInputSchema = z.object({
130+
query: z.string().describe("Search query to find posts"),
131+
subreddit: z
132+
.string()
133+
.optional()
134+
.describe(
135+
"Limit search to a specific subreddit (without 'r/' prefix). If not provided, searches all of Reddit",
136+
),
137+
sort: z
138+
.enum(["relevance", "hot", "top", "new", "comments"])
139+
.optional()
140+
.default("relevance")
141+
.describe("How to sort search results"),
142+
time: z
143+
.enum(timeFilterOptions)
144+
.optional()
145+
.default("all")
146+
.describe("Time filter: hour, day, week, month, year, all"),
147+
limit: z.coerce
148+
.number()
149+
.min(1)
150+
.max(100)
151+
.optional()
152+
.default(25)
153+
.describe("Number of results to return (1-100, default: 25)"),
154+
after: z.string().optional().describe("Pagination cursor"),
155+
});
156+
157+
/**
158+
* SEARCH_REDDIT output schema
159+
*/
160+
export const searchRedditOutputSchema = z.object({
161+
query: z.string().describe("The search query used"),
162+
subreddit: z
163+
.string()
164+
.nullable()
165+
.describe("Subreddit searched (null if all Reddit)"),
166+
sort: z.string().describe("Sort order used"),
167+
count: z.number().describe("Number of results returned"),
168+
after: z.string().nullable().describe("Pagination cursor for next page"),
169+
posts: z
170+
.array(
171+
z.object({
172+
id: z.string(),
173+
title: z.string(),
174+
author: z.string(),
175+
subreddit: z.string(),
176+
selftext: z.string(),
177+
url: z.string(),
178+
permalink: z.string(),
179+
score: z.number(),
180+
num_comments: z.number(),
181+
created_utc: z.number(),
182+
is_self: z.boolean(),
183+
flair: z.string().nullable(),
184+
nsfw: z.boolean(),
185+
}),
186+
)
187+
.describe("List of matching posts"),
188+
});

reddit/server/main.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* This is the main entry point for the Reddit MCP server.
3+
* This is a Cloudflare workers app, and serves your MCP server at /mcp.
4+
*
5+
* This MCP provides tools to interact with Reddit:
6+
* - GET_SUBREDDIT_POSTS: Fetch posts from a specific subreddit
7+
* - SEARCH_REDDIT: Search for posts across Reddit or within a subreddit
8+
*/
9+
import { DefaultEnv, withRuntime } from "@decocms/runtime";
10+
import {
11+
type Env as DecoEnv,
12+
StateSchema as BaseStateSchema,
13+
} from "../shared/deco.gen.ts";
14+
15+
import { tools } from "./tools/index.ts";
16+
17+
/**
18+
* State schema for Reddit MCP configuration.
19+
* No API key required - uses Reddit's public JSON API.
20+
*/
21+
export const StateSchema = BaseStateSchema.extend({});
22+
23+
/**
24+
* This Env type is the main context object that is passed to
25+
* all of your Application.
26+
*
27+
* It includes all of the generated types from your
28+
* Deco bindings, along with the default ones.
29+
*/
30+
export type Env = DefaultEnv &
31+
DecoEnv & {
32+
ASSETS: {
33+
fetch: (request: Request, init?: RequestInit) => Promise<Response>;
34+
};
35+
};
36+
37+
const runtime = withRuntime<Env, typeof StateSchema>({
38+
oauth: {
39+
/**
40+
* These scopes define the asking permissions of your
41+
* app when a user is installing it.
42+
* Reddit public API doesn't require authentication.
43+
*/
44+
scopes: [],
45+
/**
46+
* The state schema of your Application defines what
47+
* your installed App state will look like.
48+
* No configuration needed for Reddit public API.
49+
*/
50+
state: StateSchema,
51+
},
52+
tools,
53+
/**
54+
* Fallback directly to assets for all requests that do not match a tool or auth.
55+
*/
56+
fetch: (req: Request, env: Env) => env.ASSETS.fetch(req),
57+
});
58+
59+
export default runtime;

reddit/server/tools/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Central export point for all tools organized by domain.
3+
*
4+
* This file aggregates all tools from different domains into a single
5+
* export, making it easy to import all tools in main.ts while keeping
6+
* the domain separation.
7+
*/
8+
import { userTools } from "@decocms/mcps-shared/tools/user";
9+
import { redditTools } from "./reddit.ts";
10+
11+
// Export all tools from all domains
12+
export const tools = [...userTools, ...redditTools];
13+
14+
// Re-export domain-specific tools for direct access if needed
15+
export { userTools } from "@decocms/mcps-shared/tools/user";
16+
export { redditTools } from "./reddit.ts";

0 commit comments

Comments
 (0)