
Writing notes from inside the work
What building an MCP server taught me about where documentation should live
tobi · · 2 min read
Most writing tools assume the writing happens after the work. You finish the feature, the context evaporates, and a week later you try to reconstruct why you did it that way.
An MCP server changes where the writing can happen. Instead of a blank editor in another tab, the tool lives inside the session where the work is happening — with access to the repository, the diff, and the commit you just made.
What an MCP server actually is
It is a small program that exposes tools to a model over stdio. The model sees names, descriptions and typed arguments; it decides when to call them. Nothing more mystical than a well-described function.
Rill exposes six:
Tool | What it does |
|---|---|
| Create a post from markdown, with git provenance attached |
| Edit an existing post |
| Find something already written |
| Read a post back as markdown |
| Push a local screenshot and get a URL |
| Show what provenance would be attached |
Descriptions are the interface
The thing that surprised me most: a tool's description matters more than its signature. The model reads it to decide whether to call the tool at all.
Write the note as you would explain the work to another engineer: what the problem was, what you decided, and why.
That sentence lives in the rill_write_note description. It is not documentation — it is the instruction that shapes every note the tool produces.
Provenance comes for free
Because the server runs in your project directory, it can read git without being told anything:
export async function readGitContext(cwd: string): Promise<GitContext> {
const inRepo = await git(cwd, ['rev-parse', '--is-inside-work-tree'])
if (inRepo !== 'true') return empty()
const [remoteUrl, branch, headSha, status] = await Promise.all([
git(cwd, ['remote', 'get-url', 'origin']),
git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']),
git(cwd, ['rev-parse', 'HEAD']),
git(cwd, ['status', '--porcelain', '-uall']),
])
// …
}Every command is read-only, and every one returns null rather than throwing — provenance is a bonus on top of publishing, so it must never be the reason a post fails to save.
The result appears under the article:

Writing in the browser still works
None of this replaces the editor. Type / for blocks, or select text to format it:

What I would do differently
Read git state instead of asking the model to describe it
Upload images from disk rather than requiring a hosted URL
Stream long notes instead of sending them in one call
Let a note reference a range of commits, not just HEAD
One trap worth knowing
git status --porcelain encodes state in two leading columns, so an unstaged change looks like M path. Calling .trim() on the output eats that leading space — but only on the first line — and a fixed-width slice then removes one character too many. src/app/x.tsx becomes rc/app/x.tsx.
One wrong path, always the first, silently. Use trimEnd().