Support APIs, CLIs and MCP as first-class ways in - #3
Merged
Conversation
The board had a REST API that existed to serve the terminal client, and a CLI that could only run a board rather than use one. Neither was something you could point a script or an assistant at. This makes all three real, and makes them the same board: one client package, one permission model, no second read path. The API gains a self-describing index at /api/v1, an OpenAPI 3.1 document, and the endpoints a caller needs but a TUI never asked for — a flat forum list, a single post, a member profile, board stats. The CLI gains its other half: login/boards/use/whoami, forums, latest, topics, read, search, inbox, post and reply, against any board over the API, with --json on every one so its output is somebody else's input. MCP is new. The board serves it at /api/mcp over streamable HTTP, and tsbb-mcp serves the same tools over stdio for clients that launch a subprocess. Eleven tools; the two that write are hidden unless a token is present, so a model is never offered an action that can only fail. The HTTP endpoint honours bearer tokens and ignores cookies — browsers attach cookies to cross-origin POSTs, and that would be a write primitive for any page on the web. The tools reach the board by dispatching back through its own routes rather than the database, so an assistant is answered by the same code, the same checks and the same shapes a browser gets. Fixes a permission bug found while testing this. A hidden forum was hidden from the tree, from search and from /latest, but its permission rows still said members could read it — so anyone who knew a topic's URL, or counted upwards through the ids, was served the page. Guests included. Hidden-ness is now enforced in resolvePermissions, covering the API, the MCP tools and the HTML pages at once, and hidden ancestors count too. The TUI's client and config move to packages/client, which the TUI, the CLI and MCP now share. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnsZkEBoc39vYSN2hKLAwS
The API, CLI and MCP guides existed only as files in the repository, which
is the wrong place for the audience: somebody who lands on tsbb.dev and
wants to know whether it has an API should not have to find GitHub.
/docs and /docs/{api,cli,mcp,plugins} render the repository's own docs/*.md
through the same markdown renderer that renders posts. That is the whole
design — one copy of each document, reviewed in the pull request that
changes the behaviour it describes, and unable to quietly disagree with the
site. A hand-written HTML version always ends up describing last year's API.
The pages inherit the board's guarantees too: no client-side JavaScript, the
same stylesheet and CSP, and a set of tags that is the set the renderer
writes literally. Rendered markdown goes in a .post-body, so tables and code
blocks are styled in both skins without a line of new CSS.
Two things the rendering needed:
Paragraphs are unwrapped before rendering. The renderer turns a single
newline into a <br>, which is right for a post — somebody who pressed return
meant it — and wrong for documentation hard-wrapped at eighty columns, where
it gives a ragged column half the width of the page. The unwrapping lives in
the docs route rather than the renderer, because the renderer's behaviour is
correct for the thing it mostly renders. Code fences pass through untouched.
Links between documents are rewritten: [the MCP guide](MCP.md) resolves
beside the file on GitHub and has to be /docs/mcp on the site. Rewriting at
render time keeps both readers working, which matters because the repository
is where most people meet these documents first.
Also: the doc tables gained real headers rather than an empty header band,
the footer gained a Docs link, and /api/v1 now carries a docs URL for the
human who pasted the endpoint into a browser and found JSON.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnsZkEBoc39vYSN2hKLAwS
ralyodio
marked this pull request as ready for review
August 31, 2026 17:25
The new guides told people to `pnpm add -g @profullstack/tsbb` and `npx -y @profullstack/tsbb-mcp`. Neither package is on npm, and neither can be published as things stand: every workspace package the two binaries depend on — @tsbb/client, @tsbb/mcp, @tsbb/core, @tsbb/db, @tsbb/mail, @tsbb/plugin-host — is private, so a published tarball would resolve to nothing and fail on install. Better to document the checkout, which works today, than an install line that cannot. Making the npm path real is a separate decision with a diff of its own: publish the ten @tsbb/* packages publicly, rename them under @profullstack, or give the two binaries a prepack step that inlines their sources. All three change imports across the repository. The same false promise was in `tsbb-mcp --help`, so that is fixed too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnsZkEBoc39vYSN2hKLAwS
ralyodio
added a commit
that referenced
this pull request
Aug 31, 2026
.dockerignore excluded docs/, so the built image had no documents to render:
every /docs/{api,cli,mcp,plugins} page answered 404 in production while the
index rendered its "deployed without its docs directory" fallback and a
perfectly healthy 200. Live on tsbb.dev the moment #3 deployed.
Nothing that boots the app in a checkout could have caught it — the files
are right there — so the guard goes on the thing that was actually wrong.
test/docs.test.ts now reads .dockerignore and fails if docs/ is excluded,
and the route warns at boot when the directory is missing, because a
documentation site that silently serves nothing is a failure with no
symptom anyone would notice from the outside.
Claude-Session: https://claude.ai/code/session_01SnsZkEBoc39vYSN2hKLAwS
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The board had a REST API that existed to serve the terminal client, and a CLI that could only run a board rather than use one. Neither was something you could point a script or an assistant at. This makes all three real — and makes them the same board: one client package, one permission model, no second read path.
The API
GET /api/v1— a self-describing index. One unauthenticated request tells a client whether this is a tsbb board, where to sign in, and whether it speaks MCP.GET /api/v1/openapi.json— OpenAPI 3.1. The test walks every path in it and asks the board for it, so a description that drifts from the routes fails the suite./api/v1/forums(flat, with depth),/api/v1/posts/{id},/api/v1/users/{username},/api/v1/stats.The CLI
tsbbgains its other half. Everything below talks to a board over its API — no database, no.env, any directory, your board or anybody else's:--jsonon every command, because the reason to have a CLI rather than only a TUI is that its output is somebody else's input. Several boards at once (boards,use,--server). Failures print one line and exit non-zero.MCP
New. The board serves MCP at
/api/mcpover streamable HTTP, andtsbb-mcpserves the same tools over stdio for clients that launch a subprocess (tsbb mcpuses the board the CLI is already signed in to).Eleven tools. The two that write are hidden unless a token is present, so a model is never offered an action that can only fail; asking for one anyway gets an error saying why.
Two deliberate restrictions on the HTTP endpoint:
The tools reach the board by dispatching back through its own routes rather than the database, so an assistant is answered by the same code, the same permission checks and the same shapes a browser gets.
A permission bug, found while testing this
A hidden forum was hidden from the tree, from search and from
/latest— but its permission rows still said members could read it. So anyone who knew a topic's URL, or counted upwards through the ids, was served the page. Guests included, on the HTML pages.Hidden-ness is now enforced in
resolvePermissions, which covers the API, the MCP tools and the HTML pages at once. Hidden ancestors count too, since a visible forum inside a hidden category is how a staff area is built.Shape
The TUI's client and config move to
packages/client, shared by the TUI, the CLI and MCP.packages/mcpholds the protocol and tools, transport-agnostic;apps/mcpis the stdio binary.Testing
145 tests, up from 111.
test/api.test.ts— the index, the OpenAPI document against the real routes, the new endpoints, and MCP over HTTP: handshake, notification-gets-202, write tools gated on a token, a tool posting a reply the API can then read back, cookies refused,GET→ 405.test/cli.test.ts— binds a real port and drives the CLI over a socket, then spawnstsbb-mcpas a real subprocess and exchanges JSON-RPC over stdio. The failures worth catching there (a token not found where it was saved, a stray log line on stdout breaking the framing) only happen when it is done for real.pnpm typecheckclean. Prettier is left alone — the repo is not prettier-clean at HEAD (73 files), so running it would bury this in an unrelated diff.Second commit: the docs are served by the board
The three guides above existed only as files in the repo, which is the wrong place for the audience — somebody who lands on tsbb.dev and wants to know whether it has an API should not have to find GitHub.
/docsand/docs/{api,cli,mcp,plugins}render the repository's owndocs/*.mdthrough the same markdown renderer that renders posts. One copy of each document, reviewed in the PR that changes the behaviour it describes, and unable to quietly disagree with the site. The pages inherit the board's guarantees: no client-side JavaScript, same stylesheet, same CSP. Rendered markdown goes in a.post-body, so tables and code blocks are styled in both skins with no new CSS.Two things the rendering needed:
<br>— right for a post, wrong for prose hard-wrapped at 80 columns, where it gives a ragged column half the page width. The unwrapping lives in the docs route, not the renderer, because the renderer's behaviour is correct for the thing it mostly renders. Code fences pass through untouched.[the MCP guide](MCP.md)resolves beside the file on GitHub and has to be/docs/mcpon the site. Rewriting at render time keeps both readers working.Also: doc tables gained real headers instead of an empty header band, the footer gained a Docs link, and
/api/v1carries adocsURL for whoever pastes the endpoint into a browser and finds JSON.Known gap: neither binary is installable from npm
@profullstack/tsbband@profullstack/tsbb-mcpare both 404 on the registry, and they cannot be published as they stand: every workspace package they depend on (@tsbb/client,@tsbb/mcp,@tsbb/core,@tsbb/db,@tsbb/mail,@tsbb/plugin-host) is"private": true, so a published tarball would resolve to nothing and fail on install. This is pre-existing for the CLI and the TUI, not new here.The docs in this PR therefore document the checkout-based invocation, which works today, rather than an
npxline that would not. Making the npm path real is a separate decision — publish the ten@tsbb/*packages publicly (needs the@tsbbnpm scope), rename them under@profullstack/*, or give the two binaries a prepack step that inlines their sources — and it changes imports across the repo, so it does not belong in this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_01SnsZkEBoc39vYSN2hKLAwS