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 +6 -0
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/package.json +48 -0
- package/src/index.js +109 -0
- package/src/tools.js +215 -0
package/CHANGELOG.md
ADDED
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
|
+
}
|