Add support for reddit-mcp #51
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/mcp-beaver#51
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Picked this up in a sweep of the open queue. Empty body, so same two readings as its siblings #50 and #52, with the same question at the end.
Reading A - proxy the existing server.
serve-upstreamwraps any streamable-HTTP MCP behind a name allowlist. Nothing to build; a deploy values file.Reading B - replace the bespoke server with a guardfile. Feasible in a way bluesky is not, with two real wrinkles. From the deploy README, reddit-mcp "fetches Kai's private reddit feed URLs (env first, then SSM at call time) and serves the normalized reads over HTTP".
envor SSM through an ordinary value chain, and needs no login round-trip. This is the tractable half.Descriptor.RawResponseexists for exactly this - a non-JSON success body is written through untouched - so the bytes can reach the caller. What it will not do is normalize them, and the source repo's value is partly in that normalization. A guardfile would hand an agent raw RSS/XML and let the model parse it, which is a real downgrade in tool quality rather than a neutral swap.base-urlplus a path template assumes the secret rides in auth. A private feed URL with an embedded token has to be split acrossbase-urlandpath, or supplied as abase-url { value }chain. Workable, and worth checking rather than assuming - a token in a path segment interacts withrestrictdifferently than one in a header, and I have not verified how the two compose.So: reddit is the middle case. The auth model fits, the response format and the loss of normalization are the cost.
The finding shared across all three (#50, #51, #52): the guardfile
authblock resolves a static value and cannot perform a token exchange. Steam is easy (fixed API key), reddit is workable (tokenized URL), bluesky is blocked (app password exchanged for an expiring session).Which reading did you intend? And if B, is losing the RSS normalization acceptable, or does that argue for keeping this one bespoke?
Reading B confirmed by Kai, so I went to build it. Blocked, and I have to correct something I said above.
Correction
I wrote that
Descriptor.RawResponse"exists for exactly this - a non-JSON success body is written through untouched - so the bytes can reach the caller." That is wrong on the path this runtime uses.RawResponseis honoured only byspecverb, the CLI projection (http/specverb/request.go:465). Nothing inhttp/opcore/reads it, andopcore.Operation.Executedecodes JSON unconditionally. Verified twice against umbra v0.139.0: once through the serving path, and once by drivingOperation.Executedirectly with the field set by hand. Setting it changes nothing.An Atom body fails the whole call:
All four reddit tools return RSS or Atom, so none of them would work. Not a wrinkle - a hard block, and one I would have shipped a broken guardfile into if I had trusted the field name instead of running it.
Filed upstream as
coilyco-flight-deck/umbra#289with the evidence. Two parts: honour the flag in the opcore path, and add an inline-grammar node to set it, since the OpenAPI source infers it from a media type the inline source does not have.The other wrinkle held up
base-urldoes accept a value chain (ParseBaseURLreturns raw or chain), so a whole tokenized feed URL from env or SSM is expressible. That half is fine.The decision I would put back to you before umbra#289 gets prioritised
Even once unblocked, a guardfile returns raw Atom XML. reddit-mcp today returns normalized reads, and that normalization is a real part of its value - it is the difference between a model getting structured entries and a model parsing XML in-context on every call.
So replacing this one is a downgrade in tool quality, traded for deleting a bespoke server. For Steam that trade was clean, because the upstream already returns JSON. Here it is not.
My recommendation: keep reddit-mcp bespoke. umbra#289 is worth fixing on its own merits - a declared, exported, documented field that is silently inert on one of two execution paths is a defect regardless - but I would not fix it in order to make this migration, because the migration is not obviously worth making.
Leaving open, blocked on umbra#289 and on that call.
Decision recorded by Darren (director seat), 2026-08-16, from a full triage pass of this repo. Kai's call in a consult round.
The call
Migrate reddit to a guardfile once
coilyco-flight-deck/umbra#289lands. Staying open, blocked on that upstream rather than on a human.Kai accepted the raw-Atom cost Olaf flagged. Recording it plainly so nobody later reads it as an oversight: a guardfile returns Atom XML where reddit-mcp today returns normalized reads, so the model parses XML in context on every call. That is a measurable downgrade in tool quality traded for deleting a bespoke server, and Kai took the trade with the cost stated.
What was rejected, and why
The blocker, and the correction that matters
Descriptor.RawResponsedoes not carry a non-JSON body through on the path this runtime uses. It is honoured only byspecverb, the CLI projection athttp/specverb/request.go:465. Nothing inhttp/opcore/reads it, andopcore.Operation.Executedecodes JSON unconditionally. Verified twice against umbra v0.139.0, once through the serving path and once by drivingOperation.Executedirectly with the field set by hand. An Atom body fails the whole call withinvalid character '<' looking for beginning of value, and all four reddit tools return RSS or Atom, so none would work.So the block is umbra#289, in two parts: honour the flag in the opcore path, and add an inline-grammar node to set it, since the OpenAPI source infers it from a media type the inline source does not have.
The credential half is fine.
ParseBaseURLreturns raw or a chain, so a whole tokenized feed URL resolves from env or SSM.Acceptance
Relabelled
priority/P4autonomy/headlessrole/engineer. The human decision is discharged, so this leavesautonomy/async-consult.One caveat on that label. This cannot start until umbra#289 lands, and that is a dependency rather than a consult. This organization has no
blocked-on-dependencyreadiness label, soautonomy/headlesscurrently overstates readiness. Anyone dispatching from that queue should check umbra#289 first. Adding the label to the org is worth doing.Follow-up to my comment above, from an umbra triage pass. Darren (director seat), 2026-08-16.
When I wrote that this was blocked on
coilyco-flight-deck/umbra#289, that issue was closed. I did not check its state, and pointing a blocker at a closed issue is exactly the kind of dead reference that makes a backlog lie.Checked now, and the block is real. umbra#289 closed 27 minutes after Angie wrote in its own thread that #291 landed part one and "#291 does not close this issue - it should stay open on part two." Part two is the inline-grammar node that lets a hand-written
.mcp.kdlgrant ask for a raw body, and umbra's own shipped docs still say so -docs/specverb-raw-responses.mdcarries a Not covered here section naming this gap and calling umbra#289 open.umbra#289 is reopened, labelled
priority/P2autonomy/headlessrole/engineer, scoped to part two only. Part one stays closed as #291.So this issue's labels are unchanged and now honest: it is
autonomy/headlesswith the human decision discharged, waiting on a live upstream issue rather than a closed one. The caveat in my previous comment about this org lacking ablocked-on-dependencyreadiness label still stands.Blocker one is cleared. Blocker two is new, and I found it by running the guardfile rather than reading it. Work on
maininb937c11, leaving this open.umbra#289 is done, verified rather than assumed
raw-responseshipped as an inline node and the pin here moves v0.139.0 to v0.148.0.TestRawResponseCarriesANonJSONBodyThroughputs an Atom body through the opcore path - the exact path the earlier verification foundRawResponseinert on - and asserts the feed text reaches the caller. Its control asserts the same body without the node is still a clean decode failure, so an author who omits it gets an error rather than a silently empty read.The new blocker
examples/reddit-public.mcp.kdllints and builds exactly the URL reddit-mcp builds. Against live reddit, reproduced three times:The contrast that identifies the cause: the same URL with a named User-Agent answers 200, and with reddit-mcp's own
daily-routines/1.0 (by /u/coilysiren)answers 429 - rate limited, which is a served response rather than a block. reddit is refusing the anonymous client, not the request. reddit-mcp has always sent a descriptive agent; the inline grammar has noheadernode to set one, and opcore's client sets no default.Filed as umbra#303. It is not reddit-specific: a descriptive User-Agent is the stated etiquette for most of the volunteer and nonprofit APIs this fleet reads.
What landed anyway
examples/reddit-public.mcp.kdl, carryingget_homepage_rssandget_subreddit_rssunder the names reddit-mcp already serves, withraw-response, a courtesyrate-limit "1/1s", andcache "get_subreddit_rss" ttl="1m". Its header says THIS SPEC DOES NOT SERVE YET and names umbra#303, so nobody deploys it expecting it to work.Two things in it worth reading before it replaces the bespoke server:
restrictis looser than reddit-mcp's own check. reddit-mcp validates each name against^[A-Za-z0-9_]+$after splitting on the join character. A glob cannot express a charset. The origin is still fixed bybase-url, so this is about traversal within reddit.com rather than about arbitrary URL fetching, but it is a real narrowing the migration loses.The two private reads are not here, deliberately
get_frontpageandget_upvotedread feed URLs carrying their own token. The origin is fixed and the token rides in the query string, sopinis the right mechanism - but SSM holds those as whole URLs (/reddit/frontpage-feed-url,/reddit/upvoted-feed-url) while a pin injects a query value. Splitting the token and username into their own env vars or parameters is deploy's config to shape, not this repo's to guess, and deploy already has the ExternalSecret path reddit-mcp's own env-first design was built around.Where that leaves the acceptance
umbra#289 lands and a non-JSON body reaches the caller through opcore- done, pinned by test.The
autonomy/headlesslabel is still honest and the readiness caveat in the comment above still applies: the dependency is now umbra#303 rather than umbra#289.Blocker resolved and replaced. Darren (director seat), 2026-08-17, from a sweep against umbra's
main.umbra#289 landed, properly this time
I reopened it this morning because part two was missing. It closed again at 02:43Z, and part two is genuinely there now. Verified in umbra's tree rather than from the thread:
It is in the fail-closed unknown-node list, it refuses combination with
fail-whenbecause a raw body has nothing to evaluate, and the Not covered here section is gone fromdocs/specverb-raw-responses.md. So a hand-written.mcp.kdlgrant can now ask for a raw body, which is what this migration needed.And a new one took its place
coilyco-flight-deck/umbra#303, filed by Angie while building this very guardfile, and found by running it against live reddit rather than by reading the grammar.The inline
canbody acceptspath | query | body | method | raw-response | set | fail-when | describe. There is noheader, and opcore's client sets noUser-Agentof its own, so a generated server reaches an upstream as Go's default client.reddit refuses that specifically:
The identifying contrast: the same URL with a named User-Agent returns 200, and with reddit-mcp's own
daily-routines/1.0 (by /u/coilysiren)returns 429, a served rate-limit rather than a block. So reddit is refusing the anonymous client, not the request. reddit-mcp has always sent a descriptive agent atsrc/reddit_mcp/server.py:45; a guardfile has no way to.umbra#303 is
priority/P1autonomy/headlessrole/engineer, and the full guardfile grammar already has aheadernode athttp/guardfile/guardfile.go:593, so the inline half is mirroring something that exists rather than inventing it.Staying open, blocked, at P4
The decision is unchanged and still yours: migrate, accepting raw Atom. Two of the three blockers are now down. This is the third and last one I know of.
Worth knowing it is not reddit-only.
coilyco-bridge/deploy#470MusicBrainz states a contactable User-Agent as a mandatory source requirement rather than etiquette, so umbra#303 gates that server too.Both public reads now serve real Atom off reddit.com.
mainat40b8ca7, umbra pinned to v0.154.0.and the payload, through the coverage envelope:
Correcting my previous comment
I said the remaining blocker was the missing User-Agent and filed umbra#303 on that. It was wrong. I measured through
curl, where the anonymous agent 403s, and generalised to Go without re-running it there. From Go'snet/httpevery agent including Go's own default reaches the served path.The actual cause was the placeholder credential. Isolated from Go, one request each:
User-AgentonlyUser-Agent+Authorization: Bearer unused-public-feedAuthorization: Bearer unused-public-feedonlyThe grammar required an
authblock with no way to decline one, so a public API had to be handed a fake bearer token, and reddit rejects a token it cannot verify with 403 while serving the same request unauthenticated. That 403 reads exactly like a block on the client, which is what fooled me.auth nonelanded in umbra v0.154.0 and sends no header at all.examples/steam-storefront.mcp.kdlmoved to it too - its comment had already called the placeholder "the least-bad option" and named this gap as worth fixing upstream. It was right for a sharper reason than it knew: the placeholder is not inert, it is a wrong credential.The default User-Agent from umbra#303 stayed, on etiquette grounds alone, with the correction recorded in umbra's docs rather than left as a claimed fix.
Acceptance
umbra#289 lands and a non-JSON success body reaches the caller through the opcore path-raw-response, pinned byTestRawResponseCarriesANonJSONBodyThrough.What the private half still needs, and it is not mcp-beaver's
get_frontpageandget_upvotedread feed URLs carrying their own token. The origin is fixed and the token rides in the query string, sopinis exactly the right mechanism - it writes query parameters server-side and keeps them out of the tool schema. The obstacle is only the shape in SSM:/reddit/frontpage-feed-urland/reddit/upvoted-feed-urlhold whole URLs, and a pin injects a value.Splitting the token and username into their own parameters or env vars is a small deploy-side change, and deploy already has the ExternalSecret path reddit-mcp's env-first design was built around. Once those exist the grant is:
I have not made that change, because inventing SSM parameter names is deciding ops config from the wrong side of the boundary.
One narrowing worth recording before this replaces the bespoke server
The guardfile's
restrict subreddits matches "*"is looser than reddit-mcp's own check, which validates each name against^[A-Za-z0-9_]+$after splitting on the join character. A glob cannot express a charset. The origin stays fixed bybase-url, so this is about traversal within reddit.com rather than arbitrary URL fetching - but it is a real narrowing the migration loses, alongside the raw-Atom cost you already accepted.Leaving this open on the private half and the retirement.
All four reads now mint, and the blocker I handed to deploy was mine.
mainat847cd5f. Closing here, with the deployment and the retirement filed ascoilyco-bridge/deploy#608.Correcting my previous comment, again
I wrote that the private half "is not mcp-beaver's" and needed deploy to split the feed token and username into their own SSM parameters. I was wrong twice over.
Wrong about the ownership:
pinresolution is entirely beaver's code (internal/mcpserver/querypin.go), not umbra's. Nothing about that gap lived downstream.Wrong about the fix: splitting the credential in SSM means two copies of one secret, rotated together, with one of them going stale silently. Storing a secret twice to work around a parser is worse than parsing it. I proposed the worse option and called it deploy's config to shape.
What actually landed
from="query:<parameter>"resolves the value and then reads one query parameter out of the URL it holds. One credential, one place to rotate it, and deploy hands the pod an env var pointing at the parameter it already has.The extraction only ever narrows: a component of an already-resolved server-side value. It reaches no new source, widens no grant, and the pinned name stays out of the tool schema - which matters here more than usual, because on these feeds the token is the authorization. A value that is not a URL, or carries no such parameter, fails the call, and the error names the parameter rather than echoing the value, since a resolve failure is exactly when something gets logged.
Not reddit-specific either: private RSS and Atom feeds, signed links and webhook endpoints all arrive this shape.
One scope leak caught on the way
get_upvotedcarries the account in its path, and a path parameter cannot be pinned. Declaring it as a caller input would have handed the model "any account's upvotes" while the pin beside it was busy fixing the token - the exact failurepinexists to prevent, one parameter over. It is fixed in the path instead.The path itself is derived from reddit's documented feed shape rather than read off the real URL, and the file says so. deploy#608 carries it as a pre-flight check.
Acceptance
umbra#289 lands and a non-JSON success body reaches the caller through the opcore path-raw-response, pinned by test.A reddit guardfile serves all four reads-examples/reddit.mcp.kdlmintsget_frontpage,get_homepage_rss,get_subreddit_rss,get_upvotedunder reddit-mcp's existing names. Public two verified live against reddit.com; private two verified against a fixture that echoes the path it received, confirming the right token and account reach the wire.coilyco-bridge/deploy#608. It needs Kai's real feed URLs and it is a live-service retirement, so it stops at this repo's boundary rather than at my confidence.The two costs you accepted are carried forward in deploy#608 rather than left in a closed issue: raw Atom instead of normalized entries, and a subreddit
restrictlooser than reddit-mcp's per-name charset check.