Add support for reddit-mcp #51

Closed
opened 2026-08-12 10:19:45 +00:00 by coilysiren · 8 comments
Owner
No description provided.
Member

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-upstream wraps 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".

  • The credential is already static, which is the whole difference from #50. A feed URL carrying its own token resolves from env or SSM through an ordinary value chain, and needs no login round-trip. This is the tractable half.
  • Wrinkle 1 - the response is RSS, not JSON. The runtime normally parses and reformats JSON. Descriptor.RawResponse exists 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.
  • Wrinkle 2 - the secret is in the URL, not a header. base-url plus a path template assumes the secret rides in auth. A private feed URL with an embedded token has to be split across base-url and path, or supplied as a base-url { value } chain. Workable, and worth checking rather than assuming - a token in a path segment interacts with restrict differently 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 auth block 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?

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-upstream` wraps 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"*. * **The credential is already static**, which is the whole difference from #50. A feed URL carrying its own token resolves from `env` or SSM through an ordinary value chain, and needs no login round-trip. This is the tractable half. * **Wrinkle 1 - the response is RSS, not JSON.** The runtime normally parses and reformats JSON. `Descriptor.RawResponse` exists 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. * **Wrinkle 2 - the secret is in the URL, not a header.** `base-url` plus a path template assumes the secret rides in auth. A private feed URL with an embedded token has to be split across `base-url` and `path`, or supplied as a `base-url { value }` chain. Workable, and worth checking rather than assuming - a token in a path segment interacts with `restrict` differently 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 `auth` block 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?
Member

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.

RawResponse is honoured only by specverb, the CLI projection (http/specverb/request.go:465). Nothing in http/opcore/ reads it, and opcore.Operation.Execute decodes JSON unconditionally. Verified twice against umbra v0.139.0: once through the serving path, and once by driving Operation.Execute directly with the field set by hand. Setting it changes nothing.

An Atom body fails the whole call:

invalid character '<' looking for beginning of value

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#289 with 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-url does accept a value chain (ParseBaseURL returns 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.

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. `RawResponse` is honoured **only by `specverb`**, the CLI projection (`http/specverb/request.go:465`). Nothing in `http/opcore/` reads it, and `opcore.Operation.Execute` decodes JSON unconditionally. Verified twice against umbra v0.139.0: once through the serving path, and once by driving `Operation.Execute` directly with the field set by hand. Setting it changes nothing. An Atom body fails the whole call: ``` invalid character '<' looking for beginning of value ``` 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#289` with 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-url` does accept a value chain (`ParseBaseURL` returns 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.
Member

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#289 lands. 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

  • Keep reddit bespoke, matching the bluesky call in #50. This was the recommended option and Olaf's own recommendation. Declined.
  • Keep bespoke and commission token-exchange auth in umbra anyway. Declined. That was the bluesky-shaped option, and reddit was never blocked on auth.

The blocker, and the correction that matters

Descriptor.RawResponse does not carry a non-JSON body through on the path this runtime uses. It is honoured only by specverb, the CLI projection at http/specverb/request.go:465. Nothing in http/opcore/ reads it, and opcore.Operation.Execute decodes JSON unconditionally. Verified twice against umbra v0.139.0, once through the serving path and once by driving Operation.Execute directly with the field set by hand. An Atom body fails the whole call with invalid 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. ParseBaseURL returns raw or a chain, so a whole tokenized feed URL resolves from env or SSM.

Acceptance

  • umbra#289 lands and a non-JSON success body reaches the caller through the opcore path.
  • A reddit guardfile serves all four reads.
  • reddit-mcp is retired only after the guardfile is verified against the live private feeds.

Relabelled priority/P4 autonomy/headless role/engineer. The human decision is discharged, so this leaves autonomy/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-dependency readiness label, so autonomy/headless currently overstates readiness. Anyone dispatching from that queue should check umbra#289 first. Adding the label to the org is worth doing.

**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#289` lands.** 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 * **Keep reddit bespoke, matching the bluesky call in #50.** This was the recommended option and Olaf's own recommendation. Declined. * **Keep bespoke and commission token-exchange auth in umbra anyway.** Declined. That was the bluesky-shaped option, and reddit was never blocked on auth. ## The blocker, and the correction that matters `Descriptor.RawResponse` does **not** carry a non-JSON body through on the path this runtime uses. It is honoured only by `specverb`, the CLI projection at `http/specverb/request.go:465`. Nothing in `http/opcore/` reads it, and `opcore.Operation.Execute` decodes JSON unconditionally. Verified twice against umbra v0.139.0, once through the serving path and once by driving `Operation.Execute` directly with the field set by hand. An Atom body fails the whole call with `invalid 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. `ParseBaseURL` returns raw or a chain, so a whole tokenized feed URL resolves from env or SSM. ## Acceptance * umbra#289 lands and a non-JSON success body reaches the caller through the opcore path. * A reddit guardfile serves all four reads. * reddit-mcp is retired only after the guardfile is verified against the live private feeds. Relabelled `priority/P4` `autonomy/headless` `role/engineer`. The human decision is discharged, so this leaves `autonomy/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-dependency` readiness label, so `autonomy/headless` currently overstates readiness. Anyone dispatching from that queue should check umbra#289 first. Adding the label to the org is worth doing.
Member

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.kdl grant ask for a raw body, and umbra's own shipped docs still say so - docs/specverb-raw-responses.md carries a Not covered here section naming this gap and calling umbra#289 open.

umbra#289 is reopened, labelled priority/P2 autonomy/headless role/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/headless with 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 a blocked-on-dependency readiness label still stands.

**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.kdl` grant ask for a raw body, and umbra's own shipped docs still say so - `docs/specverb-raw-responses.md` carries a **Not covered here** section naming this gap and calling umbra#289 open. **umbra#289 is reopened**, labelled `priority/P2` `autonomy/headless` `role/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/headless` with 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 a `blocked-on-dependency` readiness label still stands.
Member

Blocker one is cleared. Blocker two is new, and I found it by running the guardfile rather than reading it. Work on main in b937c11, leaving this open.

umbra#289 is done, verified rather than assumed

raw-response shipped as an inline node and the pin here moves v0.139.0 to v0.148.0. TestRawResponseCarriesANonJSONBodyThrough puts an Atom body through the opcore path - the exact path the earlier verification found RawResponse inert 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.kdl lints and builds exactly the URL reddit-mcp builds. Against live reddit, reproduced three times:

GET https://www.reddit.com/r/golang/new/.rss?sort=new -> 403 Forbidden
<h1>whoa there, pardner!</h1>
<p>Your request has been blocked due to a network policy.</p>

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 no header node 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, carrying get_homepage_rss and get_subreddit_rss under the names reddit-mcp already serves, with raw-response, a courtesy rate-limit "1/1s", and cache "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:

  • The restrict is 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 by base-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 raw-Atom cost you recorded is in the file, marked as a decision Kai took with the cost stated rather than as an oversight.

The two private reads are not here, deliberately

get_frontpage and get_upvoted read feed URLs carrying their own token. The origin is fixed and the token rides in the query string, so pin is 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.
  • A reddit guardfile serves all four reads - blocked on umbra#303 for any of them, and on the deploy-side credential shape for the private two.
  • reddit-mcp retired after live verification - unchanged, and downstream of both.

The autonomy/headless label is still honest and the readiness caveat in the comment above still applies: the dependency is now umbra#303 rather than umbra#289.

**Blocker one is cleared. Blocker two is new, and I found it by running the guardfile rather than reading it.** Work on `main` in `b937c11`, leaving this open. ## umbra#289 is done, verified rather than assumed `raw-response` shipped as an inline node and the pin here moves v0.139.0 to v0.148.0. `TestRawResponseCarriesANonJSONBodyThrough` puts an Atom body through the **opcore** path - the exact path the earlier verification found `RawResponse` inert 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.kdl` lints and builds exactly the URL reddit-mcp builds. Against live reddit, reproduced three times: ``` GET https://www.reddit.com/r/golang/new/.rss?sort=new -> 403 Forbidden <h1>whoa there, pardner!</h1> <p>Your request has been blocked due to a network policy.</p> ``` 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 no `header` node 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`, carrying `get_homepage_rss` and `get_subreddit_rss` under the names reddit-mcp already serves, with `raw-response`, a courtesy `rate-limit "1/1s"`, and `cache "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: * **The `restrict` is 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 by `base-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 raw-Atom cost you recorded is in the file**, marked as a decision Kai took with the cost stated rather than as an oversight. ## The two private reads are not here, deliberately `get_frontpage` and `get_upvoted` read feed URLs carrying their own token. The origin is fixed and the token rides in the query string, so `pin` is 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. * A reddit guardfile serves all four reads - **blocked on umbra#303** for any of them, and on the deploy-side credential shape for the private two. * reddit-mcp retired after live verification - unchanged, and downstream of both. The `autonomy/headless` label is still honest and the readiness caveat in the comment above still applies: the dependency is now umbra#303 rather than umbra#289.
Member

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:

http/opcore/inline.go:230   case "raw-response":
http/opcore/inline.go:231       return applyInlineRawResponse(d, c)
http/opcore/inline.go:288   unknown node %q (want path | query | body | method | raw-response | set | fail-when | describe; fail-closed)
http/opcore/inline.go:296   if d.RawResponse && d.FailWhen != "" { ... cannot be combined ... }

It is in the fail-closed unknown-node list, it refuses combination with fail-when because a raw body has nothing to evaluate, and the Not covered here section is gone from docs/specverb-raw-responses.md. So a hand-written .mcp.kdl grant 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 can body accepts path | query | body | method | raw-response | set | fail-when | describe. There is no header, and opcore's client sets no User-Agent of its own, so a generated server reaches an upstream as Go's default client.

reddit refuses that specifically:

GET https://www.reddit.com/r/golang/new/.rss?sort=new -> 403 Forbidden
whoa there, pardner! Your request has been blocked due to a network policy.

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 at src/reddit_mcp/server.py:45; a guardfile has no way to.

umbra#303 is priority/P1 autonomy/headless role/engineer, and the full guardfile grammar already has a header node at http/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#470 MusicBrainz states a contactable User-Agent as a mandatory source requirement rather than etiquette, so umbra#303 gates that server too.

**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: ``` http/opcore/inline.go:230 case "raw-response": http/opcore/inline.go:231 return applyInlineRawResponse(d, c) http/opcore/inline.go:288 unknown node %q (want path | query | body | method | raw-response | set | fail-when | describe; fail-closed) http/opcore/inline.go:296 if d.RawResponse && d.FailWhen != "" { ... cannot be combined ... } ``` It is in the fail-closed unknown-node list, it refuses combination with `fail-when` because a raw body has nothing to evaluate, and the **Not covered here** section is gone from `docs/specverb-raw-responses.md`. So a hand-written `.mcp.kdl` grant 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 `can` body accepts `path | query | body | method | raw-response | set | fail-when | describe`. There is no `header`, and opcore's client sets no `User-Agent` of its own, so a generated server reaches an upstream as Go's default client. reddit refuses that specifically: ``` GET https://www.reddit.com/r/golang/new/.rss?sort=new -> 403 Forbidden whoa there, pardner! Your request has been blocked due to a network policy. ``` 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 at `src/reddit_mcp/server.py:45`; a guardfile has no way to. umbra#303 is `priority/P1` `autonomy/headless` `role/engineer`, and the full guardfile grammar already has a `header` node at `http/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#470` MusicBrainz states a contactable User-Agent as a **mandatory** source requirement rather than etiquette, so umbra#303 gates that server too.
Member

Both public reads now serve real Atom off reddit.com. main at 40b8ca7, umbra pinned to v0.154.0.

{"level":"INFO","msg":"tool call served","tool":"get_homepage_rss","duration_ms":487,"outcome":"ok"}

and the payload, through the coverage envelope:

{"coverage":{"truncated":false,"bytes":53381,"over_budget":true},
 "result":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\" ...>"}

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's net/http every agent including Go's own default reaches the served path.

The actual cause was the placeholder credential. Isolated from Go, one request each:

Headers Response
a named User-Agent only 429, the ordinary rate limiter
User-Agent + Authorization: Bearer unused-public-feed 403 Forbidden
Authorization: Bearer unused-public-feed only 403 Forbidden

The grammar required an auth block 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 none landed in umbra v0.154.0 and sends no header at all. examples/steam-storefront.mcp.kdl moved 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 by TestRawResponseCarriesANonJSONBodyThrough.
  • A reddit guardfile serves all four reads - two of four, both public, verified live. The private two are below.
  • reddit-mcp retired after live verification - not yet, and downstream of the private half.

What the private half still needs, and it is not mcp-beaver's

get_frontpage and get_upvoted read feed URLs carrying their own token. The origin is fixed and the token rides in the query string, so pin is 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-url and /reddit/upvoted-feed-url hold 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:

pin "get_frontpage" {
    query "feed" env "REDDIT_FRONTPAGE_FEED_TOKEN"
    query "user" env "REDDIT_FEED_USER"
}

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 by base-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.

**Both public reads now serve real Atom off reddit.com.** `main` at `40b8ca7`, umbra pinned to v0.154.0. ``` {"level":"INFO","msg":"tool call served","tool":"get_homepage_rss","duration_ms":487,"outcome":"ok"} ``` and the payload, through the coverage envelope: ```json {"coverage":{"truncated":false,"bytes":53381,"over_budget":true}, "result":"<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\" ...>"} ``` ## 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's `net/http` every agent including Go's own default reaches the served path. The actual cause was the **placeholder credential**. Isolated from Go, one request each: | Headers | Response | | --- | --- | | a named `User-Agent` only | 429, the ordinary rate limiter | | `User-Agent` + `Authorization: Bearer unused-public-feed` | **403 Forbidden** | | `Authorization: Bearer unused-public-feed` only | **403 Forbidden** | The grammar required an `auth` block 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 none` landed in umbra v0.154.0 and sends no header at all. `examples/steam-storefront.mcp.kdl` moved 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 by `TestRawResponseCarriesANonJSONBodyThrough`. * **A reddit guardfile serves all four reads** - **two of four**, both public, verified live. The private two are below. * reddit-mcp retired after live verification - not yet, and downstream of the private half. ## What the private half still needs, and it is not mcp-beaver's `get_frontpage` and `get_upvoted` read feed URLs carrying their own token. The origin is fixed and the token rides in the query string, so `pin` is 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-url` and `/reddit/upvoted-feed-url` hold 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: ```kdl pin "get_frontpage" { query "feed" env "REDDIT_FRONTPAGE_FEED_TOKEN" query "user" env "REDDIT_FEED_USER" } ``` 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 by `base-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.
Member

All four reads now mint, and the blocker I handed to deploy was mine. main at 847cd5f. Closing here, with the deployment and the retirement filed as coilyco-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: pin resolution 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

pin "get_frontpage" {
    query "feed" env "REDDIT_FRONTPAGE_FEED_URL" from="query:feed"
    query "user" env "REDDIT_FRONTPAGE_FEED_URL" from="query:user"
}

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_upvoted carries 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 failure pin exists 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.kdl mints get_frontpage, get_homepage_rss, get_subreddit_rss, get_upvoted under 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.
  • reddit-mcp retired after live verification - 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 restrict looser than reddit-mcp's per-name charset check.

**All four reads now mint, and the blocker I handed to deploy was mine.** `main` at `847cd5f`. Closing here, with the deployment and the retirement filed as `coilyco-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: `pin` resolution 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 ```kdl pin "get_frontpage" { query "feed" env "REDDIT_FRONTPAGE_FEED_URL" from="query:feed" query "user" env "REDDIT_FRONTPAGE_FEED_URL" from="query:user" } ``` `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_upvoted` carries 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 failure `pin` exists 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.kdl` mints `get_frontpage`, `get_homepage_rss`, `get_subreddit_rss`, `get_upvoted` under 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. * **reddit-mcp retired after live verification** - `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 `restrict` looser than reddit-mcp's per-name charset check.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/mcp-beaver#51
No description provided.