reddit-mcp
by brettadams0
README.md
# reddit-mcp
[](https://github.com/brettadams0/reddit-mcp/actions/workflows/ci.yml)
[](https://github.com/brettadams0/mcp-surface)
[](LICENSE)
[](package.json)
An [MCP](https://modelcontextprotocol.io) server over the Reddit API, backed by a self-owned OAuth2 app. It **can act as the account** — post,
comment, vote, and send private messages — so it is worth being deliberate about
what you ask it to do.
12 tools. Runs locally over stdio.
## Tools
**Read**
| Tool | Purpose |
|---|---|
| `reddit_get_me` | The authenticated account's own profile |
| `reddit_get_user` | Another user's public profile |
| `reddit_get_user_activity` | A user's recent posts and comments |
| `reddit_get_subreddit_posts` | Listing for a subreddit (hot/new/top/rising) |
| `reddit_get_post` | A single post plus its comment tree |
| `reddit_search` | Search across Reddit or within one subreddit |
| `reddit_get_inbox` | Private messages and inbox replies |
**Write — these have real, public consequences**
| Tool | Purpose |
|---|---|
| `reddit_submit_post` | Create a new post in a subreddit |
| `reddit_submit_comment` | Reply to a post or comment |
| `reddit_vote` | Up/down/clear vote on a post or comment |
| `reddit_send_message` | Send a private message to a user |
| `reddit_mark_read` | Mark inbox items read |
The write tools post under your real account, publicly and attributably. Reddit's
spam and vote-manipulation rules apply to API traffic exactly as they do to
browser traffic, and account bans follow the account, not the app. Every tool
declares MCP annotations (`readOnlyHint` and friends), so a client can
auto-approve the reads and prompt for the writes.
The exact schemas the model sees are in [`mcp-surface.json`](mcp-surface.json).
## Design notes
### Reddit says "200 OK" when it means no
Posting, commenting and messaging use `api_type=json`, and Reddit reports their
failures — a subreddit that doesn't exist, a rate limit, a locked thread — as
HTTP 200 with the reasons tucked into `json.errors`. Passed straight through,
that body let the model tell you a post was live when nothing had been posted.
Those responses are now turned into errors that name Reddit's reasons.
The same quiet-200 behaviour makes ids dangerous. Reddit addresses things by
*fullname* — a type prefix plus a base36 id (`t1_` comment, `t3_` post, `t4_`
message) — and its write endpoints accept a wrong-typed fullname and do nothing.
Every id is normalised and type-checked before the request: a comment id passed
where a post is required is an error, and a bare id is only accepted where its
type is unambiguous.
### Path segments are validated, not just encoded
Subreddit names, usernames and post ids are interpolated into the request path,
and URL resolution collapses `..` — so a subreddit of `../api/v1/me` used to
resolve to `oauth.reddit.com/api/v1/me/hot`, with the account's bearer token
attached. `encodeURIComponent` doesn't help: it leaves `..` alone. Each segment
is now checked against what Reddit actually allows. The tool arguments are model
output, and a model that has just read a page can be steered by it, so this is
input validation in the ordinary web-server sense.
### Token refresh is single-flight
Clients call tools in parallel. When several calls find the access token
expired at once, they share one refresh rather than each starting their own, so
a burst of tool calls costs one request to Reddit's token endpoint. A refresh
that Reddit answers with an error, even one sent as HTTP 200, is retried on the
next call rather than cached.
## Auth
OAuth2 with a refresh token, stored in `credentials/token.json` (gitignored).
Reddit does **not** rotate refresh tokens, so the stored one stays valid
indefinitely — access tokens are refreshed automatically ~60s before expiry.
```bash
npm run authorize # one-time browser consent, writes credentials/token.json
npm run check-auth # verify the stored token still works
```
The consent flow checks an OAuth `state` and its callback listener binds to
loopback only.
## Setup
Requires Node 20+.
1. Create a **script** app at [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps)
with redirect URI `http://localhost:53683/callback`.
2. Save its id and secret to `credentials/client_secret.json` — see
`credentials/client_secret.example.json` for the shape. `credentials/` is
git-ignored.
3. Identify yourself to Reddit. The default User-Agent contains a placeholder,
and Reddit throttles generic agents hard, so set one of:
```bash
REDDIT_USERNAME=your_reddit_username # fills in the /u/ segment
REDDIT_USER_AGENT="platform:app:v1.0.0 (by /u/you)" # or replace it wholesale
```
4. Authorize and register:
```bash
npm ci
npm run authorize
claude mcp add reddit -e REDDIT_USERNAME=your_reddit_username -- node /absolute/path/to/reddit-mcp/src/index.js
```
Or in Claude Desktop's `claude_desktop_config.json`:
```json
{
"mcpServers": {
"reddit": {
"command": "node",
"args": ["/absolute/path/to/reddit-mcp/src/index.js"],
"env": { "REDDIT_USERNAME": "your_reddit_username" }
}
}
}
```
## Development
```bash
npm test # unit tests — no network, no credentials
npm run lint # ESLint
npm run surface # start the server and diff its tool surface against mcp-surface.json
npm run surface:update # re-record mcp-surface.json after an intended change
```
The tests cover registration, id and path-segment validation (including the
traversal cases above, asserted at the tool handlers), the `json.errors`
check, single-flight refresh, rate-limit and timeout messages, and the
User-Agent. CI runs them on Node 20, 22 and 24, then starts the server with
[mcp-surface](https://github.com/brettadams0/mcp-surface) and fails if the tool
surface differs from the committed snapshot.
## Layout
```
src/shared.js version, timeouts, error shaping, tool-result helpers
src/auth.js token load, single-flight refresh, authenticated fetch
src/reddit.js id/path validation and all tool registrations
src/index.js McpServer construction + stdio transport
scripts/authorize.js one-time OAuth consent flow
scripts/check-token.js token health check
mcp-surface.json the committed tool surface CI compares against
```
## Notes
- Reddit requires a descriptive, unique `User-Agent`; a generic one gets 429s
regardless of rate. A 429 is reported with the reset time Reddit sends.
- Every request has a 30-second timeout, and error bodies are trimmed before
they reach the model — an HTML error page would otherwise land in its context
whole.
## License
MIT — see [LICENSE](LICENSE). Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
TDQS
A3.5/5.0
Scored across 12 tools
Disambiguation5/5
All 12 tools have clearly distinct purposes, e.g., reddit_get_me vs reddit_get_user, reddit_get_subreddit_posts vs reddit_search. No two tools appear to do the same thing.
Naming Consistency5/5
Every tool uses the consistent pattern reddit_verb_noun in snake_case, e.g., reddit_get_post, reddit_submit_post, reddit_mark_read. Naming is predictable and uniform.
Tool Count5/5
12 tools is well-scoped for a Reddit client, covering reading (posts, user, subreddit, search, inbox) and writing (submit post/comment, vote, message, mark read) without being excessive.
Completeness4/5
Core CRUD/CUD operations are covered, but missing edit and delete for posts/comments, as well as subreddit info and user subscriptions. Minor gaps for a full Reddit surface.
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues