feat(reddit): authenticated Reddit MCP server with saved-item search - #106
feat(reddit): authenticated Reddit MCP server with saved-item search#106jwaldrip wants to merge 2 commits into
Conversation
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
|
🚅 Deployed to the han-pr-106 environment in han-team-platform 4 services not affected by this PR
|
Code ReviewReviewed the new MODERATE
MINOR
Nice details worth calling out (no action needed)
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
|
Acted on all four points in 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
2. 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 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 |
Verification ReviewAll four original concerns addressed in
LGTM - all concerns addressed. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Summary
The
redditplugin ranuvx 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
packages/mcp-server-reddit(auth.ts,client.ts,format.ts,tools.ts,index.ts)get_me,get_saved,search_saved,get_upvoted,get_downvoted,get_hidden,get_my_posts,get_my_comments,get_subscribed_subreddits,get_inbox,get_multiredditssearch_savedpages saved history and matches locally on title, body, parent post title, subreddit, author, flair, and URL, reportingscannedandmatchedso a miss is distinguishable from an incomplete scansearch_reddit,get_user_profile.mcp.jsonnow runs the new server overnpx; drops theuvand Python prerequisite in favor of Nodepublish-mcp-servers.ymlworkflow, modeled onpublish-bridge-plugins.yml(npm trusted publishing via OIDC, no token)redditandhashi-redditmarketplace entries updated in sync (alias preserved)Auth modes
Resolved from the environment, strongest first:
userappanonymousThe server always starts. In
appandanonymousmode 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
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:
Validation:
marketplace.jsonre-validated as JSON; 340 entries preserved,redditandhashi-redditboth intactTesting
bun test) covering credential resolution, mode selection, URL construction per auth mode, formatting, and saved-item matchingbiome check, clean)tsc --noEmit)get_savedunauthenticated returnsisErrorwith setup instructionsNot verified: live calls against Reddit. The build environment's egress is 403'd by Reddit at the IP level (reproduced with plain
curlagainst bothwww.reddit.comandoauth.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
Opened by the
fa-archimedesfield agent from a spoken instruction. Left unmerged for review, per routine.Generated by Claude Code