feat(examples): bluesky's seven credential-free reads as a guardfile #92

Merged
coilysiren merged 1 commit from feat/bluesky-public-guardfile into main 2026-08-19 23:16:47 +00:00
Member

Half of retiring bluesky-mcp. Companion to #91, which carries the other half.

What this is

bluesky-mcp registers eleven read tools and logs in for all eleven, because it holds one authenticated BskyAgent and every handler awaits it. That made the whole surface look like it needed a credential. Seven of them do not. Bluesky's public AppView serves them unauthenticated, and this guardfile is those seven.

Tool names match the bespoke server exactly, so the public half is a drop-in.

Measured, not assumed

Every status was probed against public.api.bsky.app on 2026-08-19, and every grant was then served through a locally built mcp-beaver serve and called end to end.

Shipped here, all returning 200 live through the guardfile:

  • get_profile, search_profiles, get_author_feed, get_posts, get_post_thread, list_followers, list_follows

Left behind, and each for a measured reason:

  • get_home_timeline - app.bsky.feed.getTimeline, 401
  • list_notifications - app.bsky.notification.listNotifications, 401
  • get_kai_liked_posts - app.bsky.feed.getActorLikes, 400
  • search_posts - app.bsky.feed.searchPosts, 403, which is the one worth flagging. It sits beside app.bsky.actor.searchActors, which answers 200, so "search" splits across the auth boundary in a way that reads as arbitrary until measured. That asymmetry is upstream's.

All four need a session bootstrap, which no auth scheme in this repo can express. That is #91.

Verification performed

  • just lint-examples exits 0 with the new file included, listing all seven grants plus mcp_beaver_info.
  • Live smoke through mcp-beaver serve examples/bluesky-public.mcp.kdl: all seven grants returned real payloads with the coverage-first envelope intact.
  • Bound enforcement confirmed to fail before the upstream sees a request:
    • limit=999 returns query field "limit": value 999 is above maximum 50
    • uris=[] returns query field "uris": array length 0 is below min-items 1
  • The uris array projects to repeated upstream keys, verified against a real post URI.

One deliberate break from drop-in

search_profiles takes term, not query, which is what bluesky-mcp calls it. query is a reserved opcore engine flag and fails closed at lint - the same wall steam-storefront.mcp.kdl hit and documented. Renaming the tool instead would have been worse, since the tool name is what a model reaches for. The file says so at the point of the rename.

What is lost, recorded in the file rather than discovered later

The trailing comment block names three things, none of which blocks the swap and all of which are real:

  1. Charset validation is gone. bluesky-mcp validates actor, uri, and cursor against regexes. restrict ... matches is a glob, applies to path parameters, and these are all query parameters, so there is no equivalent. What remains is the metachar gate plus the declared types and bounds. Exposure is small because base-url is fixed, so no input can redirect the origin.
  2. Schema defaults are gone. The bespoke server defaults limit to 20 or 25 per tool and depth/parentHeight to 3. Bounds are declared here, defaults are not, so an omitted argument now takes the upstream default. Worth a look before this ships.
  3. Viewer state is gone. An authenticated AppView read carries viewer.following, viewer.muted, and friends. An unauthenticated one does not. Irrelevant for reading what an account posts, decisive for anything reasoning about Kai's relationship to an account.

What it gains

Every result leads with coverage and carries the payload under result, so a truncating harness keeps the caveat rather than destroying it. That is the first invariant of coilyco-gaming/steam-ops#18, arriving for free.

Note for the reviewer, not caused by this branch

pre-commit run reports two pre-existing documentation-layout failures on docs/s3.md (79 lines against a 40-line cap, 3513 chars against 3000). That file is untouched here and fails the same way on a clean main. Every hook that evaluates this branch's file passes, code-comments included. Flagging it because it will block anyone committing to this repo until it is split.

Refs #91.

🤖 Generated with Claude Code

Half of retiring `bluesky-mcp`. Companion to #91, which carries the other half. ## What this is `bluesky-mcp` registers eleven read tools and logs in for all eleven, because it holds one authenticated `BskyAgent` and every handler awaits it. That made the whole surface look like it needed a credential. Seven of them do not. Bluesky's public AppView serves them unauthenticated, and this guardfile is those seven. Tool names match the bespoke server exactly, so the public half is a drop-in. ## Measured, not assumed Every status was probed against `public.api.bsky.app` on 2026-08-19, and every grant was then served through a locally built `mcp-beaver serve` and called end to end. Shipped here, all returning 200 live through the guardfile: * `get_profile`, `search_profiles`, `get_author_feed`, `get_posts`, `get_post_thread`, `list_followers`, `list_follows` Left behind, and each for a measured reason: * `get_home_timeline` - `app.bsky.feed.getTimeline`, 401 * `list_notifications` - `app.bsky.notification.listNotifications`, 401 * `get_kai_liked_posts` - `app.bsky.feed.getActorLikes`, 400 * `search_posts` - `app.bsky.feed.searchPosts`, **403**, which is the one worth flagging. It sits beside `app.bsky.actor.searchActors`, which answers 200, so "search" splits across the auth boundary in a way that reads as arbitrary until measured. That asymmetry is upstream's. All four need a session bootstrap, which no auth scheme in this repo can express. That is #91. ## Verification performed * `just lint-examples` exits 0 with the new file included, listing all seven grants plus `mcp_beaver_info`. * Live smoke through `mcp-beaver serve examples/bluesky-public.mcp.kdl`: all seven grants returned real payloads with the `coverage`-first envelope intact. * Bound enforcement confirmed to fail **before** the upstream sees a request: * `limit=999` returns `query field "limit": value 999 is above maximum 50` * `uris=[]` returns `query field "uris": array length 0 is below min-items 1` * The `uris` array projects to repeated upstream keys, verified against a real post URI. ## One deliberate break from drop-in `search_profiles` takes `term`, not `query`, which is what `bluesky-mcp` calls it. `query` is a reserved opcore engine flag and fails closed at lint - the same wall `steam-storefront.mcp.kdl` hit and documented. Renaming the tool instead would have been worse, since the tool name is what a model reaches for. The file says so at the point of the rename. ## What is lost, recorded in the file rather than discovered later The trailing comment block names three things, none of which blocks the swap and all of which are real: 1. **Charset validation is gone.** `bluesky-mcp` validates `actor`, `uri`, and `cursor` against regexes. `restrict ... matches` is a glob, applies to path parameters, and these are all query parameters, so there is no equivalent. What remains is the metachar gate plus the declared types and bounds. Exposure is small because `base-url` is fixed, so no input can redirect the origin. 2. **Schema defaults are gone.** The bespoke server defaults `limit` to 20 or 25 per tool and `depth`/`parentHeight` to 3. Bounds are declared here, defaults are not, so an omitted argument now takes the upstream default. Worth a look before this ships. 3. **Viewer state is gone.** An authenticated AppView read carries `viewer.following`, `viewer.muted`, and friends. An unauthenticated one does not. Irrelevant for reading what an account posts, decisive for anything reasoning about Kai's relationship to an account. ## What it gains Every result leads with `coverage` and carries the payload under `result`, so a truncating harness keeps the caveat rather than destroying it. That is the first invariant of coilyco-gaming/steam-ops#18, arriving for free. ## Note for the reviewer, not caused by this branch `pre-commit run` reports two pre-existing `documentation-layout` failures on `docs/s3.md` (79 lines against a 40-line cap, 3513 chars against 3000). That file is untouched here and fails the same way on a clean `main`. Every hook that evaluates this branch's file passes, `code-comments` included. Flagging it because it will block anyone committing to this repo until it is split. Refs #91. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(examples): bluesky's seven credential-free reads as a guardfile
All checks were successful
ci / gate (push) Successful in 37s
ci / publish (push) Has been skipped
ci / gate (pull_request) Successful in 36s
ci / publish (pull_request) Has been skipped
878cd25fbc
The first half of retiring coilyco-flight-deck/bluesky-mcp. That server
registers eleven read tools and logs in for all eleven, because it holds one
authenticated BskyAgent every handler awaits. Seven of them never needed the
credential: Bluesky's public AppView serves them unauthenticated.

Verified against public.api.bsky.app rather than assumed. All seven grants
return 200 there, and the four left behind do not - getTimeline and
listNotifications 401, getActorLikes 400, and searchPosts 403 despite sitting
beside searchActors, which answers 200. Those four need a session bootstrap,
which no auth scheme here can express, tracked as #91.

Tool names match bluesky-mcp exactly, so the public half is a drop-in with one
exception recorded in the file: search_profiles takes `term` rather than
`query`, because `query` is a reserved opcore engine flag and fails closed at
lint. steam-storefront.mcp.kdl hit the same wall.

The trailing comment block names what this loses against the server it
replaces - charset validation on actor, uri, and cursor, the per-tool schema
defaults, and viewer state, which unauthenticated AppView reads do not carry.
None blocks the swap and all three are real.

Refs #91.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!92
No description provided.