All notes

Teaching an assistant to log film, without giving it your keys

Rollbook now speaks MCP, so an assistant can log and update your rolls directly. The interesting part wasn't the API — it was deciding what an assistant is never allowed to do on its own.

An assistant can now read and write your Rollbook logbook directly — log a new roll as you load the camera, mark one as at the lab, add a development note, ask which rolls have been waiting longest. rollbook-mcp is on npm, and the setup is one command. None of that was the hard part.

The hard part was deciding what an assistant should never be able to do by itself, and then making that decision structural instead of a rule I hoped I'd remember.

The one thing that stays manual, on purpose

Rollbook is a browser app talking straight to Supabase. There was no server, so there was no way for anything but a signed-in browser to touch a roll. Giving an assistant access meant building one: a credential it could hold, and a write path that enforced everything the browser already enforced.

The credential is an API token, minted by a Postgres function that generates it with the database's own random source and returns it exactly once — only its hash is ever stored. That part is a fairly ordinary security pattern. The decision underneath it is the one that mattered: minting a token has to stay something only a signed-in human does in a browser. There is no endpoint for it. An assistant setting this up has to stop and ask the person to go create one, every single time. That's not a missing feature — it's the whole point. The day an assistant can provision its own credential is the day it can also provision one for itself without you noticing.

Sharing gets its own door

Rollbook lets a roll be shared — an unlisted public page, a QR code that resolves for anyone who scans it. An assistant with write access could plausibly decide, while tidying up your log, to just publish a roll. So publishing isn't a field you can set through the ordinary update call. It's a separate route, publish_roll, flagged destructive, with its own confirmation. Rotating a roll's public link is separated out the same way, because it silently breaks every QR sticker already printed on a canister — a physical consequence an assistant has no way to see coming unless the tool description says so in plain words.

The general update endpoint actively rejects a sharing field if you try to sneak it in, and names the correct route instead of quietly ignoring it. An error an assistant can't act on just produces a retry loop. One it can act on produces the right behavior on the next call.

The bug that would have destroyed real data

Every roll's metadata lives in one jsonb column, and now two completely separate pieces of code write to it — the browser and the new server function, one in TypeScript, one in Deno. They have to agree on the shape byte for byte, and I found the reason why while checking my own database: five of my ten real rolls carry a field called statusManual that exists nowhere in the app's source. It survives purely because the browser's mapping function copies every key on a roll rather than rebuilding it from a known list.

If I'd written the new server path with a tidy, explicit field list — which is the instinctively "cleaner" way to write it — the first edit an assistant made to any of those five rolls would have silently deleted that field forever. No error, no warning, just gone. The fix was to copy everything, unconditionally, exactly like the original code does, and then prove it by patching one of those rolls through the new path and checking the database directly for what survived.

What actually went wrong

Not that. What went wrong was smaller and dumber: I built the write path and never checked that the role it runs as had permission to touch the tables. Row-level security decides which rows a role can see; a grant decides whether the role can touch the table at all. I had the first without the second, four separate times, on four separate tables — the fix for each one was a one-line grant, and each one took a full round of "why is this 500ing" to find. Correct policy on an ungranted table just fails, and it fails in a way that looks like everything else is broken.

Getting it in front of people

Wiring the pieces together was one kind of work. Making it something a stranger's assistant could set up without me in the loop was a different kind. The package is on npm now, so the install is npx -y rollbook-mcp with no clone and no build — three places point at that: the package's own README, an install skill in the repo, and an llms.txt on the live site, because an agent asked to install something looks in different places than a person does. All three say the same thing about the token, because that's the one instruction that actually matters.

The database work, the write path, the token lifecycle, and the distribution were four different problems that happened to arrive in one feature. The token boundary is the only one I'd call a real decision. Everything else was just making sure the new door didn't let in anything the old one wouldn't have.