Skip to content

Support APIs, CLIs and MCP as first-class ways in - #3

Merged
ralyodio merged 3 commits into
mainfrom
worktree-tsbb-api-cli-mcp
Aug 31, 2026
Merged

Support APIs, CLIs and MCP as first-class ways in#3
ralyodio merged 3 commits into
mainfrom
worktree-tsbb-api-cli-mcp

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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.
  • New endpoints a caller needs but a TUI never asked for: /api/v1/forums (flat, with depth), /api/v1/posts/{id}, /api/v1/users/{username}, /api/v1/stats.

The CLI

tsbb gains its other half. Everything below talks to a board over its API — no database, no .env, any directory, your board or anybody else's:

tsbb login forum.example.com
tsbb forums | latest | topics <forum> | read <id> | search <words…> | inbox
tsbb post general "Title" < body.md
tsbb reply 42 "…"

--json on 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/mcp over streamable HTTP, and tsbb-mcp serves the same tools over stdio for clients that launch a subprocess (tsbb mcp uses 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:

  • Bearer tokens only — it ignores the board's session cookie, even a valid one. Browsers attach cookies to cross-origin POSTs, so honouring one would be a write primitive for any page on the web.
  • Writes need a token — a guest reads whatever the board shows guests.

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/mcp holds the protocol and tools, transport-agnostic; apps/mcp is 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 spawns tsbb-mcp as 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.
  • The hidden-forum fix is locked down across the post endpoint, the topic endpoint, the topic list, the HTML pages as a guest, and the MCP tools.

pnpm typecheck clean. 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.

/docs and /docs/{api,cli,mcp,plugins} render the repository's own docs/*.md through 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:

  • Paragraphs are unwrapped before rendering. The renderer turns a single newline into a <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.
  • 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.

Also: doc tables gained real headers instead of an empty header band, the footer gained a Docs link, and /api/v1 carries a docs URL for whoever pastes the endpoint into a browser and finds JSON.

Known gap: neither binary is installable from npm

@profullstack/tsbb and @profullstack/tsbb-mcp are 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 npx line that would not. Making the npm path real is a separate decision — publish the ten @tsbb/* packages publicly (needs the @tsbb npm 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

ralyodio and others added 2 commits August 31, 2026 17:15
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
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
ralyodio merged commit 6d9e73b into main Aug 31, 2026
2 of 3 checks passed
@ralyodio
ralyodio deleted the worktree-tsbb-api-cli-mcp branch August 31, 2026 20:46
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant