Skip to content

feat(reddit): authenticated Reddit MCP server with saved-item search - #106

Draft
jwaldrip wants to merge 2 commits into
mainfrom
claude/awesome-cannon-qygeae
Draft

feat(reddit): authenticated Reddit MCP server with saved-item search#106
jwaldrip wants to merge 2 commits into
mainfrom
claude/awesome-cannon-qygeae

Conversation

@jwaldrip

@jwaldrip jwaldrip commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

The reddit plugin ran uvx mcp-server-reddit: public, unauthenticated, read-only. It could read all of Reddit except the part that belongs to you. If you use Reddit's save button as a reading list, that archive was unreachable, and Reddit offers no search over it even when you are signed in.

This adds @thebushidocollective/mcp-server-reddit, a Node MCP server that speaks Reddit's OAuth API, and repoints the plugin at it.

Changes

  • New package packages/mcp-server-reddit (auth.ts, client.ts, format.ts, tools.ts, index.ts)
  • New account tools (require auth): get_me, get_saved, search_saved, get_upvoted, get_downvoted, get_hidden, get_my_posts, get_my_comments, get_subscribed_subreddits, get_inbox, get_multireddits
  • search_saved pages saved history and matches locally on title, body, parent post title, subreddit, author, flair, and URL, reporting scanned and matched so a miss is distinguishable from an incomplete scan
  • New public tools: search_reddit, get_user_profile
  • All eight previous public tool names and parameters preserved, so existing prompts and the memory provider keep working
  • Plugin .mcp.json now runs the new server over npx; drops the uv and Python prerequisite in favor of Node
  • Memory provider now prefers the user's saved archive over the public firehose
  • New publish-mcp-servers.yml workflow, modeled on publish-bridge-plugins.yml (npm trusted publishing via OIDC, no token)
  • Plugin bumped to 2.0.0 with a changelog entry; both reddit and hashi-reddit marketplace entries updated in sync (alias preserved)

Auth modes

Resolved from the environment, strongest first:

Mode Trigger Behavior
user client id plus refresh token, or client id plus username and password All tools
app client id only Public tools at app rate limits
anonymous no credentials Public tools

The server always starts. In app and anonymous mode the account tools are still listed and return the exact environment variables to set and where to get them, so a missing credential is diagnosable rather than invisible. This follows the graceful-degradation shape in .claude/rules/language-plugins/lsp-entrypoint-pattern.md.

Safety

Every tool is read only. There are no voting, posting, commenting, or saving tools, so the server cannot modify a Reddit account. Token request failures never echo the response body, which can contain the submitted grant.

Type of Change

  • New feature
  • Breaking change
  • Documentation

Breaking in one respect only: the plugin's runtime prerequisite moves from uv/Python to Node 20+. Tool names and parameters are unchanged.

Plugin Changes

Plugin category:

  • Integration - MCP servers for external services

Validation:

  • marketplace.json re-validated as JSON; 340 entries preserved, reddit and hashi-reddit both intact
  • Hooks tested in Claude Code session (N/A, no hooks)

Testing

  • 29 unit tests pass (bun test) covering credential resolution, mode selection, URL construction per auth mode, formatting, and saved-item matching
  • Linting passes (biome check, clean)
  • Typecheck passes (tsc --noEmit)
  • Manual testing performed: MCP stdio handshake lists all 21 tools; get_saved unauthenticated returns isError with setup instructions

Not verified: live calls against Reddit. The build environment's egress is 403'd by Reddit at the IP level (reproduced with plain curl against both www.reddit.com and oauth.reddit.com), so no request path could be exercised end to end. URL construction is covered by unit tests instead. First real use with credentials is the remaining check.

Checklist

  • Code follows existing patterns
  • Self-review completed
  • Documentation updated (plugin README rewritten, package README added, changelog entry)
  • No new warnings

Opened by the fa-archimedes field agent from a spoken instruction. Left unmerged for review, per routine.


Generated by Claude Code

The reddit plugin ran `uvx mcp-server-reddit`, which is public,
unauthenticated, and read-only. It could read all of Reddit except the part
that belongs to you.

This adds `@thebushidocollective/mcp-server-reddit`, a Node MCP server that
speaks Reddit's OAuth API, and points the plugin at it.

New account tools (require auth): get_me, get_saved, search_saved,
get_upvoted, get_downvoted, get_hidden, get_my_posts, get_my_comments,
get_subscribed_subreddits, get_inbox, get_multireddits.

search_saved is the headline. Reddit offers no server side search over saved
items, so it pages saved history and matches locally on title, body, parent
post title, subreddit, author, flair, and URL.

All eight previous public tool names and parameters are preserved, so
existing prompts and memory providers keep working. search_reddit and
get_user_profile are added alongside them.

Three auth modes, resolved from the environment:
  user       client id plus refresh token, or client id plus user and pass
  app        client id only, public reads at app rate limits
  anonymous  no credentials, public reads

The server always starts. In app and anonymous mode the account tools are
still listed and return the exact environment variables to set, so a missing
credential is diagnosable rather than invisible.

Every tool is read only. There are no voting, posting, commenting, or saving
tools, so the server cannot modify a Reddit account.

Also drops the uv and Python prerequisite in favor of Node, adds a
publish-mcp-servers workflow modeled on publish-bridge-plugins (npm trusted
publishing via OIDC), and keeps both the reddit and hashi-reddit marketplace
entries in sync.

Verified: biome clean, tsc clean, 29 unit tests pass, and an MCP stdio
handshake lists all 21 tools with the unauthenticated guard returning setup
instructions. Live Reddit calls were not verifiable from the build
environment, which Reddit 403s by IP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKHkorFedq5MN12v2xzLWP
@railway-app

railway-app Bot commented Aug 9, 2026

Copy link
Copy Markdown

🚅 Deployed to the han-pr-106 environment in han-team-platform

4 services not affected by this PR
  • postgres
  • redis
  • api
  • website

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

Code Review

Reviewed the new @thebushidocollective/mcp-server-reddit package (auth, client, tools, format) and the plugin/workflow wiring. Overall this is well-structured: good separation of concerns (auth/client/format/tools), sensible OAuth token caching with in-flight de-duplication, graceful degradation to anonymous/app mode, and error messages that avoid echoing sensitive response bodies. A few things worth a look:

MODERATE

  1. No tests for TokenProvider or RedditClient.get/getListing/collectListing (packages/mcp-server-reddit/src/auth.ts, client.ts). These contain the most security- and correctness-sensitive logic in the package: token caching/expiry skew, concurrent-refresh collapsing (inFlight), the 401-invalidate-and-retry path, and 429/5xx backoff with retry-after handling. Only buildRedditUrl and the pure auth.ts helpers (readCredentials, resolveMode, explainMissingUserAuth) are tested. A fetch mock (e.g. via bun:test's mock or a small stub) exercising token refresh, 401 retry-once behavior, and the pagination loop in collectListing would catch regressions here that are otherwise easy to introduce silently (e.g. an off-by-one in EXPIRY_SKEW_MS, or the inFlight promise not being cleared correctly under concurrent callers).

  2. search_saved can issue up to max_pages (default 10, max 20) sequential Reddit requests per call (tools.ts), each subject to the retry/backoff logic in client.get. In the worst case (repeated 429/5xx across pages) a single tool call could take a long time before returning truncated: true. Consider documenting an approximate worst-case latency in the tool description, or exposing the scan as incremental progress, though this is a reasonable tradeoff given Reddit has no server-side search over saved items.

MINOR

  1. RedditClient.get's 401 handling doesn't back off before retrying (client.ts): unlike the 429/5xx branch, the 401 && token branch calls this.tokens.invalidate() and immediately loops to fetch a new token and retry, with no sleep. This is probably fine in practice (an immediate retry after invalidating a stale token is reasonable), but it's an asymmetry worth a one-line comment if intentional, since every other failure path in this loop sleeps.

  2. Password-grant credentials (REDDIT_USERNAME/REDDIT_PASSWORD) are documented as "no 2FA" — worth double-checking this is still accurate; Reddit has been tightening restrictions on the legacy password grant for script apps over time, and users following the README could hit invalid_grant if the grant type is deprecated for their app. Not a code bug, just something to verify empirically since the code degrades gracefully either way (the explainMissingUserAuth message will show if it fails to establish user mode... though note: if the password grant starts working but Reddit later revokes it mid-session, fetchToken will throw a clear error, which is fine).

Nice details worth calling out (no action needed)

  • auth.ts's fetchToken explicitly avoids echoing the response body on failure so a leaked grant/secret doesn't end up in tool output — good defensive practice.
  • explainMissingUserAuth gives actionable setup steps instead of a bare failure, consistent with the "never refuse to start" design goal stated in the file header.
  • .mcp.json's ${REDDIT_CLIENT_ID}-style env interpolation matches the existing pattern used by plugins/frameworks/gluestack/.mcp.json, so this isn't introducing a new/unproven convention.
  • Marketplace alias entries (hashi-reddit) were correctly updated alongside the canonical reddit entry, per the plugin-aliases convention in this repo.
  • formatThing/formatThings trims Reddit's ~100-field payloads down to a small stable shape — good for keeping tool output out of context budget.

Nothing here blocks merging; the MODERATE items are the ones I'd prioritize (test coverage for the token/retry logic in particular) but neither is a correctness bug in the current code path.

Addresses review on #106. The token and retry logic was the most
correctness-sensitive code in the package and the only part not under test.

Adds 27 tests against a stubbed fetch:

TokenProvider: grant selection across all three modes, basic auth header and
user agent, expiry-skew caching either side of the 60s boundary, concurrent
refresh collapsing to a single request, in-flight clearing after a failed
refresh (a wedge here would break every later call), invalidate forcing
re-auth, and the guarantee that a failure message never echoes the response
body, asserted with a grant value the test looks for by name.

RedditClient: bearer and user agent propagation, 401 invalidating and
retrying with a genuinely new token, a persistent 401 giving up instead of
looping, 429 and 5xx retried, 404 not retried, anonymous mode issuing no
token and targeting the public host with the .json suffix, listing
normalization including a listing with no data, and collectListing paging on
the cursor, forwarding caller parameters to every page, honoring maxPages
and limit, capping each page at Reddit's 100, and stopping on an empty page.

Also from the same review:
- Comment the one retry path that deliberately does not back off. A 401 means
  the token is wrong, not that Reddit is overloaded, so the next attempt mints
  a new one; the attempt counter still bounds it.
- Note the sequential request cost of search_saved in its tool description,
  so a deep scan is a chosen cost rather than a surprise.
- Soften the password grant docs. Reddit has been narrowing where it is
  accepted, so say invalid_grant means the grant was refused and point at the
  refresh token, rather than implying 2FA is the only failure mode.

56 tests pass, biome and tsc clean, handshake still serves 21 tools.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKHkorFedq5MN12v2xzLWP

jwaldrip commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Acted on all four points in 0d3de82.

1. Untested token and retry logic (moderate). Correct, and it was the right thing to flag first: that code was the most sensitive in the package and the only part with no coverage. Added 27 tests against a stubbed fetch, taking the suite from 29 to 56.

TokenProvider: grant selection across all three modes, basic auth header and user agent, expiry-skew caching asserted either side of the 60s boundary, concurrent refresh collapsing to one request, inFlight clearing after a failed refresh (a wedge there would poison every later call, which is the failure you named), invalidate() forcing re-auth, and the no-echo guarantee asserted with a grant value the test looks for by name.

RedditClient: 401 invalidating and retrying with a genuinely new token (asserted by counting tokens issued, not just requests made), a persistent 401 giving up instead of looping, 429 and 5xx retried, 404 not retried, anonymous mode issuing no token and hitting the public host with the .json suffix, and collectListing paging on the cursor, forwarding caller parameters to every page, honoring maxPages and limit, capping each page at 100, and stopping on an empty page.

2. search_saved worst-case latency (moderate). Documented in the tool description rather than restructured: it now states the sequential request cost and suggests lowering max_pages when only recent saves matter. Incremental progress is the better answer but needs a streaming shape the tool contract does not have here.

3. 401 does not back off (minor). Intentional, now commented. A 401 says the token is wrong, not that Reddit is overloaded, so the next attempt mints a new one rather than repeating the same request; the attempt counter still bounds it. The comment says exactly that, and the "gives up after repeated 401s" test pins the bound.

4. Password grant and 2FA (minor). Good catch, and I could not verify it empirically for the same reason the PR body already flags: Reddit 403s this build environment at the IP level. So I fixed the claim rather than the code. Both READMEs now say the grant is the weaker option, that Reddit has been narrowing where it is accepted, and that an invalid_grant means the grant was refused rather than a bad password, pointing at the refresh token. Asserting "works, except with 2FA" was more confidence than I had earned.

Nothing here changed a runtime code path except the added comment and the tool description string. Still biome and tsc clean, 56 tests pass, handshake still serves 21 tools.


Generated by Claude Code

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

Verification Review

All four original concerns addressed in 0d3de82:

  1. No tests for TokenProvider/RedditClient retry logic — ✅ ADDRESSED. 27 new tests against a stubbed fetch cover token caching/expiry-skew boundaries, concurrent-refresh collapsing, in-flight clearing after failed refresh, 401 invalidate-and-retry, 429/5xx backoff, and collectListing pagination edge cases.
  2. search_saved worst-case latency — ✅ ADDRESSED via discussion/docs. Tool description now documents the sequential request cost and suggests lowering max_pages.
  3. 401 retry has no backoff — ✅ ADDRESSED. Now has an explanatory comment plus a test pinning the "gives up after repeated 401s" bound.
  4. Password-grant/2FA claim accuracy — ✅ ADDRESSED via discussion. Author couldn't verify empirically (Reddit blocks the build environment's IP) but softened the README claim to reflect actual confidence level.

LGTM - all concerns addressed.

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants