Skip to main content
Glama
pmsorhaindo

reddit-mcp

by pmsorhaindo
README.md
# reddit-mcp

Read-only **Reddit Data API** MCP connector for **Cursor / Grok Bot** connect-card OAuth.
Runs as a **Cloudflare Worker** that speaks **MCP Streamable HTTP** and **passes through** the caller's `Authorization: Bearer` token to `https://oauth.reddit.com`.

Repository: https://github.com/pmsorhaindo/reddit-mcp

## Architecture

```
Cursor / Grok Bot
   │  OAuth (connect-card) against Reddit authorize/token
   │  then MCP JSON-RPC with Bearer access token
   ▼
Cloudflare Worker (this repo)
   │  POST /mcp  → tools/list, tools/call
   │  GET  /.well-known/oauth-*  → discovery pointing at Reddit
   ▼
https://oauth.reddit.com  (User-Agent from env, Bearer passthrough)
```

| Route | Purpose |
| --- | --- |
| `GET /` or `/health` | Liveness + endpoint map |
| `POST /mcp` | MCP Streamable HTTP (JSON-RPC: `initialize`, `tools/list`, `tools/call`) |
| `GET /.well-known/oauth-authorization-server` | OAuth AS metadata (Reddit authorize/token) |
| `GET /.well-known/oauth-protected-resource` | Protected-resource metadata for this MCP URL |
| `GET /callback` | Dev landing page only — **does not** exchange codes |

### Why minimal JSON-RPC (not the full MCP SDK)?

`@modelcontextprotocol/sdk` historically targeted Node transports. This Worker implements a **Workers-native**, dependency-light Streamable HTTP JSON-RPC surface (`initialize` / `tools/list` / `tools/call`) that matches what Cursor/Grok need for tool calling, without Durable Objects or session stores. That fits **bearer passthrough**: the Worker never holds Reddit refresh tokens.

### Tools

| Tool | Reddit API | Scopes |
| --- | --- | --- |
| `get_me` | `GET /api/v1/me` | `identity` |
| `list_subscriptions` | `GET /subreddits/mine/subscriber` | `mysubreddits` |
| `get_user_history` | `GET /user/{user}/{overview\|submitted\|comments}` | `history`, `read` |
| `get_subreddit_feed` | `GET /r/{sub}/{hot\|new\|top\|rising\|controversial}` | `read` |
| `search_reddit` | `GET /search` or `/r/{sub}/search` | `read` |

All tools return **compact JSON** (truncated text fields). `limit` defaults to **25**, capped at **100**.

### Rate limiting

- Outbound Reddit calls are **serialized** per Worker isolate (promise chain).
- Response `X-Ratelimit-Remaining` / `X-Ratelimit-Reset` are honored before the next call.
- HTTP **429** / **503** trigger bounded retries with `Retry-After` or exponential backoff.

### Secret hygiene

**Never commit real secrets.**

- Strong `.gitignore` covers `.env`, `.dev.vars`, `.wrangler/`, `node_modules/`, `dist/`, `credentials*.json`, `.mcp-auth/`.
- Only **example** env files are committed: `.dev.vars.example`, `.env.example`.
- `wrangler.toml` has **no** inline secrets.
- Errors and `console` logs never include `Authorization` headers, tokens, or secret env values.
- Tool error payloads are status/message only — no upstream response bodies that might echo credentials.

## Reddit app setup

1. Create a Reddit app at https://www.reddit.com/prefs/apps (type **web app** or **installed app** as appropriate for your client).
2. Add redirect URIs:
   - `http://localhost:8787/callback` — local wrangler / manual testing
   - `https://www.cursor.com/agents/mcp/oauth/callback` — Cursor connect-card
3. Request scopes (comma-separated in Reddit authorize URLs):
   - `identity,read,mysubreddits,history`
4. Note `client_id` / `client_secret` for the **OAuth client** (Cursor / your secrets manager). **Do not** put them in this repo or in `wrangler.toml`.

### OAuth discovery assumptions

- `/.well-known/oauth-authorization-server` advertises:
  - `authorization_endpoint`: `https://www.reddit.com/api/v1/authorize`
  - `token_endpoint`: `https://www.reddit.com/api/v1/access_token`
- This Worker **does not** perform authorization-code exchange and **does not** store refresh tokens.
- Cursor/Grok complete OAuth with Reddit (using the Reddit app credentials you configure in the client), then send `Authorization: Bearer <access_token>` on MCP requests.
- `issuer` in the metadata is this Worker's origin so MCP clients can discover docs from the MCP base URL; authorize/token still happen on Reddit.
- Reddit scopes are traditionally **comma-separated** on the authorize URL — clients must join scopes accordingly.

## Cloudflare deploy

Prerequisites: Node 22+, Cloudflare account, Wrangler logged in (`npx wrangler login`).

```bash
git clone https://github.com/pmsorhaindo/reddit-mcp.git
cd reddit-mcp
npm install

# Local secrets (gitignored)
cp .dev.vars.example .dev.vars
# edit USER_AGENT — must identify you per Reddit API rules

npm run typecheck
npm run dev          # http://127.0.0.1:8787

# Production secret (not committed)
npx wrangler secret put USER_AGENT
npm run deploy
```

`USER_AGENT` example shape (placeholder only):

```text
cloudflare-worker:reddit-mcp:0.1.0 (by /u/YOUR_REDDIT_USERNAME)
```

After deploy, note the workers.dev or custom domain URL, e.g. `https://reddit-mcp.<account>.workers.dev`.

## Add MCP server (Cursor / Grok)

Configure **AddMcpServer** (or equivalent connect-card) with:

| Field | Value |
| --- | --- |
| URL | `https://<your-worker>/mcp` |
| Auth | OAuth (connect-card) |
| Scopes | `identity` `read` `mysubreddits` `history` |

The client should use your Reddit app's client id/secret and the Cursor redirect URI above. After auth, MCP tool calls must include the Reddit access token as a Bearer header; this Worker forwards it upstream.

## Smoke tests

Replace `$BASE` and `$TOKEN` locally — **do not** commit tokens.

```bash
BASE=http://127.0.0.1:8787
TOKEN=reddit_access_token_here

# Health
curl -sS "$BASE/health"

# OAuth metadata
curl -sS "$BASE/.well-known/oauth-authorization-server"
curl -sS "$BASE/.well-known/oauth-protected-resource"

# MCP initialize
curl -sS -X POST "$BASE/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}'

# tools/list
curl -sS -X POST "$BASE/mcp" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# tools/call get_me (requires valid Bearer)
curl -sS -X POST "$BASE/mcp" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_me","arguments":{}}}'
```

Expected: JSON-RPC results; `get_me` returns compact profile fields. Missing Bearer → JSON-RPC error `Unauthorized` without leaking headers.

## Scripts

| Script | Command |
| --- | --- |
| `npm run dev` | `wrangler dev` |
| `npm run deploy` | `wrangler deploy` |
| `npm run typecheck` | `tsc --noEmit` |

## Out of scope

- Writing, voting, messaging, moderation, or Ads API
- Storing Reddit refresh tokens or client secrets on the Worker
- Performing OAuth code exchange on `/callback`
- Full MCP resources/prompts/sampling/elicitation sessions
- Multi-tenant token vaults / Durable Object session stores
- Guaranteeing cross-isolate global rate-limit coordination (limits are per isolate + Reddit headers)

## License

Private / unpublished unless you add a license file.