reddit-radar-mcp
by sourav2024
README.md
# reddit-radar-mcp
Find Reddit threads where your product genuinely fits, reconstruct the conversation, and
gate every drafted reply against a claim boundary you define.
**Read-only by design.** There is no code path that posts, votes, or acts as an account,
and a test asserts there never will be. Drafts are for a human to review, edit, and post.
[](https://github.com/sourav2024/reddit-radar-mcp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/reddit-radar-mcp)
[](https://nodejs.org)
[](LICENSE)
```bash
npx reddit-radar-mcp # run as an MCP server
npm install reddit-radar-mcp # or use the scoring/gate functions directly
```
Requires Node 20.10+. No build step, no native dependencies.
## Why this exists
The usual "social listening" tool finds mentions. That is the easy half. The hard half is
everything after: is this thread actually relevant, what is the person really asking, and
is the reply you are about to post *true*?
This package is built around three claims that came out of running it in production:
1. **Keyword matching produces mostly garbage.** A recency + question-form + "any
recommendations" heuristic scores 50/100 on literally any recent Reddit post. The fix
is the anchor rule (below), and it is the most important thing here.
2. **Where a thread lives changes what you should say.** The same question in a buyer
subreddit and an engineering subreddit warrant different comments, so subreddit tiers
set *behaviour*, not just ranking.
3. **A model writing promotional copy is the worst possible judge of whether it
overclaimed.** So the claim gate is deterministic, rule-based, and runs server-side.
It refuses to hand back a blocked draft.
## Quick start
Write a config:
Name it `.mjs` unless your project already sets `"type": "module"` — otherwise Node parses
it as CommonJS and the import fails.
```js
// radar.config.mjs
import { defineConfig, packs, composePacks } from 'reddit-radar-mcp';
export default defineConfig({
product: {
name: 'Acme',
what: 'CI/CD pipeline observability.',
claims: ['flaky test detection', 'build timing breakdowns'],
},
queries: ['flaky tests', 'CI pipeline slow', 'build times'],
// REQUIRED. Without it, every recent question looks like an opportunity.
domainTerms: ['ci', 'pipeline', 'flaky', 'github actions', 'test suite'],
// Words that mean something else outside your niche.
ambiguousTerms: ['build', 'runner'],
tiers: {
tier1: { mode: 'PROMOTE', weight: 20, subreddits: ['devops'] },
tier2: { mode: 'PROMOTE_SOFT', weight: 15, subreddits: ['sre', 'kubernetes'] },
tier3: { mode: 'CONTRIBUTE', weight: 8, subreddits: ['ExperiencedDevs'] },
tier4: { mode: 'TECHNICAL_ONLY', weight: 3, subreddits: ['programming'] },
},
gate: {
...composePacks(packs.noPricing, packs.noFabricatedMetrics, packs.noCustomerNames),
productPattern: /\bAcme\b/i,
unsupported: [
{ term: /\bJenkins\b/i, why: 'No Jenkins integration exists.' },
],
},
});
```
Register it as an MCP server:
```bash
claude mcp add radar --scope user \
-e RADAR_CONFIG=/abs/path/radar.config.mjs \
-- npx reddit-radar-mcp
```
Then just talk to your agent: *"run a sweep and show me what's worth replying to."*
## The anchor rule
The single most useful idea in this package.
A post is **anchored** only if something ties it to your domain: real domain vocabulary,
an unambiguous query match, or a configured subreddit. Signals that describe the *shape*
of a post — it is recent, it is a question, it says "recommendations" — can never carry a
post on their own.
Without this gate, those shape signals sum to 40+ and pass anything. With it, a post in
r/podcasts asking about a "POD episode" stops outranking a genuine buying question.
Two related behaviours fall out of the same idea:
- **Ambiguous terms** ("build", "POD", "detention") only count when a second domain signal
is present — *or* when the post is in one of your subreddits, since the subreddit is
itself domain context.
- **Venting is penalized hard** (-35). Rants out-engage buying questions, so without this
the ranking inverts and you get "opportunities" that are people complaining about their
coworkers.
## Engagement modes
Tiers attach a mode to every result, and the sweep output repeats it next to each thread:
| Mode | Meaning |
|---|---|
| `PROMOTE` | Name the product, describe the fitting capability, disclose affiliation. |
| `PROMOTE_SOFT` | Answer first. Mention the product only if they are asking for tooling. |
| `CONTRIBUTE` | Share insight. Product only as context for who you are. |
| `TECHNICAL_ONLY` | **Do not pitch.** Nobody there is buying; promo gets removed. |
## The draft gate
`check_draft` runs two independent checks and refuses to return a blocked draft.
**Claim gate** (`factCheck`) — deterministic rules over your claim boundary. Starter packs
cover the four common failure modes:
| Pack | Blocks |
|---|---|
| `noPricing` | Dollar figures, per-unit rates, price-tier comparisons |
| `noFabricatedMetrics` | Invented percentages, uptime/SLA claims, unverifiable scale |
| `noCustomerNames` | Client references (even anonymous), case studies with measured outcomes |
| `noMarketingSpeak` | "leverage", "seamless", "robust", "game-changing" (WARN) |
| `requireDisclosure` | Naming your product without disclosing affiliation |
Two behaviours worth knowing:
- **Denials are always allowed.** "We do not support Jenkins" passes. An early version
blocked it, which pushed drafts toward silence about gaps — the opposite of the intent.
Conceding a real limitation is the cheapest credibility available.
- **Capability checks are assertion-scoped.** "Jenkins is a solid choice if you need
self-hosting" does not trip the gate, because it is not a claim about *your* product.
**Quality gate** (`styleCheck`) — catches text that reads as unedited generated filler:
em dashes, semicolons, curly quotes, negation framing ("not just X, it's Y"), marketing
vocabulary, flat sentence rhythm, and thin substance.
This is **not** AI-detection evasion. It cannot be and does not try to be. Many subreddits
ban *low-effort* content, and moderators read comments rather than running classifiers. So
the gate enforces what that rule actually asks for: real substance, no filler. The human
still edits and posts, and disclosure is always present.
Pass your domain vocabulary so the substance check knows what a specific noun looks like:
```js
styleCheck(draft, { anchorTerms: [...config.domainTerms, ...config.featureTerms] });
```
## MCP tools
| Tool | Does | LLM cost |
|---|---|---|
| `plan_sweep` | Returns search URLs + the page extractor to run on each | none |
| `ingest_sweep` | Dedupes, scores, ranks swept results into an opportunity list | none |
| `score_thread` | 0–100 score for one post with per-point reasoning | none |
| `analyze_thread` | Reconstructs a thread + returns binding claim constraints | none |
| `parse_thread_html` | Same, from client-side browser extraction | none |
| `check_draft` | **The enforcement point.** APPROVED or BLOCKED | none |
| `get_claim_boundary` | What may and may not be claimed | none |
Every tool is deterministic. The model supplies the writing; the server supplies the facts
and the veto.
## Reddit access
Three interchangeable adapters behind one interface:
- **`BrowserRedditClient`** — reads the same public pages a person reads, from your own
browser tool. No credentials. This is the default path today.
- **`RedditApiClient`** — OAuth against the official Data API. Access is approval-gated;
see [docs/REDDIT-ACCESS.md](docs/REDDIT-ACCESS.md).
- **`FixtureRedditClient`** — local JSON fixtures for tests and development.
Fixtures run through the **same normalizers** as live responses, so parsers are genuinely
exercised rather than first meeting real data in production.
A caveat worth stating plainly: browser mode depends on Reddit's DOM, and Reddit ships
redesigns. The extractors are written to fail loudly rather than silently return empty
threads that look like "no discussion found."
## Programmatic use
```js
import { scoreRelevance, factCheck, styleCheck, packs, composePacks } from 'reddit-radar-mcp';
import config from './radar.config.js';
const result = scoreRelevance(post, config, { matchedQueries: ['flaky tests'] });
if (result.passed) console.log(result.score, result.reasons);
const gate = factCheck(draft, config.gate);
if (!gate.allowed) console.log(gate.findings);
```
## Ethics and policy
This tool exists to help you find conversations you can genuinely contribute to. It will
not help you astroturf.
- **No posting automation.** Not implemented, and enforced by test.
- **Disclose affiliation.** `requireDisclosure` is on by default. Undisclosed vendor
comments get removed and can earn a permanent ban, which ends the channel entirely.
- **One account.** Reddit's Responsible Builder Policy prohibits registering multiple
accounts for the same use case. Do not use this to run a sockpuppet network.
- **Threads are scored, never people.** Nothing here profiles an author, in line with
Reddit's prohibition on inferring user characteristics.
- **Respect subreddit rules.** `TECHNICAL_ONLY` exists because pitching in the wrong place
is both rude and counterproductive.
## Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `RADAR_CONFIG` | — | **Required.** Absolute path to your config (`.js` ESM with a default export, or `.json`). |
| `REDDIT_MODE` | `browser` | `browser`, `live`, or `fixture`. See [docs/REDDIT-ACCESS.md](docs/REDDIT-ACCESS.md). |
| `REDDIT_CLIENT_ID` | — | `live` mode only. |
| `REDDIT_CLIENT_SECRET` | — | `live` mode only. |
| `REDDIT_USER_AGENT` | — | `live` mode only. Must be `<platform>:<appid>:<version> (by /u/<user>)`. |
| `REDDIT_QPM` | `60` | Rate limit for `live` mode. Lower than Reddit's advertised 100 on purpose. |
| `RADAR_LOG_LEVEL` | `info` | `silent`, `error`, `warn`, `info`, `debug`. |
| `RADAR_LOG_FORMAT` | `json` | `json` or `text`. |
Full annotated list in [.env.example](.env.example).
Logs go to **stderr only**. On stdio transport stdout carries the JSON-RPC protocol, so
anything written there corrupts the stream. Credentials in URLs and sensitive keys are
redacted before logging.
## Troubleshooting
**Everything scores as an opportunity.** Your `domainTerms` are too generic or missing.
That list is what anchors a post to your domain, and without it the shape signals carry
posts on their own. Config validation treats an empty list as an error for this reason.
**Nothing scores at all.** Check that `domainTerms` uses words that actually appear in post
titles. Terms of 5+ characters match simple inflections (`pipeline` → `pipelines`); shorter
ones match exactly, so `app` will not match `apps`.
**A good draft is blocked as thin substance.** Pass your vocabulary as `anchorTerms` — the
MCP server does this from your config automatically, but a direct `styleCheck()` call needs
it explicitly.
**An honest limitation is blocked.** It should not be; denials are explicitly allowed.
Please [report it](https://github.com/sourav2024/reddit-radar-mcp/issues).
**Reddit shows "Prove your humanity".** A cold search can hit a JS challenge. Loading any
subreddit page first usually clears it for the session.
**"Cannot use import statement outside a module".** Your config is a `.js` file in a
project without `"type": "module"`, so Node parses it as CommonJS. Either name it
`radar.config.mjs` or add `"type": "module"` to the nearest `package.json`. A `.json`
config avoids the question entirely, at the cost of regex literals and `composePacks`.
More in [SUPPORT.md](SUPPORT.md).
## Tests
```bash
npm test # 33 unit tests
npm run smoke # 14 checks over the real MCP wire protocol
npm run verify # everything, including the metadata consistency guard
```
The safety suite asserts that no client exposes a write method, no source file references
a Reddit write endpoint, and the package exports no posting function.
## Contributing
Issues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Note the permanent
exclusions listed there: posting automation, multi-account support, and AI-detection
evasion are deliberate non-goals rather than missing features.
- [Changelog](CHANGELOG.md)
- [Security policy](SECURITY.md)
- [Support](SUPPORT.md)
## Support this project
If this saves you time, [sponsoring on GitHub](https://github.com/sponsors/sourav2024)
helps keep it maintained. Entirely optional — the package is MIT and always will be.
Non-financial contributions are just as useful: a bug report with a reproducing config, a
rule pack that generalizes, or a note about a scoring case that surprised you.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues