berbotu-mcp
# berbotu-mcp
An MCP server that lets an AI read and write real records in a live repository — without being able to break
anything quietly.
It sits over the vault behind a small record label: releases, demos, and label state, all markdown with YAML
frontmatter. Four tools over stdio. Three read, one writes.
```
"what demos came in this week I haven't decided on yet?" → list_demos { stage: "Listening" }
"what's the next release scheduled?" → list_releases { stage: "Scheduled", limit: 1 }
"log the demo that just came in from X" → add_demo ✍️ writes a real git commit
```
---
## Why it's built the way it is
The interesting problem isn't connecting an AI to data. That's an afternoon. The problem is that **an AI with
write access is an intern who never asks twice** — so every design decision here is about what happens when it's
wrong.
**Every write is its own git commit, through the GitHub Contents API.**
Not a filesystem write. A commit. Which means every single thing the AI ever did is attributable, diffable, and
revertible with one command. If it writes garbage at 3am, I don't need a backup — I need `git revert`. This costs
an API round-trip per write and it's worth it.
**Existence is SHA-checked before anything is touched.**
`getVaultFile` resolves the file's SHA first. Passing a `sha` to the create endpoint turns a create into an
*overwrite* — so the check isn't defensive, it's the difference between "this demo already exists" and silently
destroying a record.
**Inputs are validated at the boundary, then again in the handler.**
Tool inputs are zod schemas — stage is an enum, so a hallucinated stage gets a clean rejection with the valid
options instead of writing nonsense. Then `addDemo` re-validates the same fields itself. The code calls this
*defence in depth*: the schema is the contract, but a future caller might not go through it.
**Tools carry honest annotations.**
```
readOnlyHint: false — it writes
destructiveHint: true — appends a commit to a real git repo
idempotentHint: false — calling twice with the same args creates two demos
```
These are hints to the model about what it can safely retry. `add_demo` is *not* idempotent and saying otherwise
would invite exactly the double-write it warns about.
**Read-only first, writes later.**
Stages 1–2 shipped with no write path at all. `add_demo` landed only once the read tools had been running against
the real vault long enough to trust the parsing. There was no deadline — that's just the order that made the
mistakes cheap.
**Errors return, they don't throw.**
Every tool returns `isError: true` with a human-readable reason — missing env var, missing folder, malformed
frontmatter, 401, 422, network down. An MCP server that throws gives the model a stack trace to hallucinate
around. One that explains gives it something to say to the user.
---
## The bug that justifies all of it
While building the data layer, `list_releases` returned **1 of 12 releases**. No error. No warning. Just a
confident, wrong, almost-empty list.
The cause: `gray-matter`'s default YAML engine treats a duplicate map key as a **fatal** parse error and returns
`{}` for the whole file. The vault had duplicate keys in real files — a separate script had been appending
`ig_posted: true` twice. Eleven records were being silently dropped on the floor.
Fix was the `yaml` package with `{ uniqueKeys: false }` — lenient, last-duplicate-wins, parses all twelve.
The point isn't the fix. **The point is that nothing failed.** No exception, no red text. If I'd trusted the
output, an AI would have been confidently telling me I had one release. That's the whole reason this repo is
paranoid: the dangerous failures are the quiet ones.
---
## Quick start
```bash
npm install
npm run build # TS → dist/
npm run inspect # MCP Inspector web UI, call the tools by hand
```
Attach to Claude Desktop — add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"berbotu": {
"command": "node",
"args": ["/path/to/berbotu-mcp/dist/index.js"],
"env": { "BERBOTU_VAULT_PATH": "/path/to/vault" }
}
}
}
```
Read tools need `BERBOTU_VAULT_PATH`. `add_demo` also needs `VAULT_WRITE_PAT` — a fine-grained GitHub PAT scoped
to one repo with `Contents: Read and write`. See `.env.example`.
## Tools
| Tool | Args | Returns |
|---|---|---|
| `ping` | none | version, vault path, pid, node — confirms it's alive and pointed at the right vault |
| `list_demos` | `stage?` `limit?` | `[stage] artist — title ⭐score (received YYYY-MM-DD)`, newest first |
| `list_releases` | `stage?` `includeEps?` `includeTracks?` `limit?` | `[stage] YYYY-MM-DD · artist — title (single\|EP · genre)` |
| `add_demo` ✍️ | `artist` `track` `stage?` `receivedDate?` `notes?` | creates a demo file as a GitHub commit |
## Honest limits
- **stdio only.** Local. HTTP transport + JWT auth was the next stage and hasn't been built — there was no reason to.
- **One writer.** No conflict handling beyond the SHA existence check. Fine for one operator, wrong for a team.
- **Read tools are filesystem, writes are API.** A deliberate split — reads want to be fast and local, writes want
to be auditable. It does mean a write isn't visible to a read until the repo syncs.
- **Four tools.** Small on purpose. Every tool with write access is a thing that can be wrong at 3am.
## Stack
TypeScript · `@modelcontextprotocol/sdk` · `zod` · `yaml` · GitHub Contents API · stdio
*Built AI-assisted. I'm not an engineer and I'm not trying to be — I'm someone who knows what to build and how to
keep it from breaking things.*
TDQS
Scored across 4 tools
Each tool has a clearly distinct purpose: ping for health check, list_demos for demo submissions, list_releases for catalog releases, and add_demo for creating new entries. No overlapping resource or action.
Tools follow a consistent verb_noun pattern with snake_case (list_demos, list_releases, add_demo). ping is a simple verb but is standard and does not disrupt the pattern.
Four tools is small but well-scoped for a niche vault/music label server. It covers health, listing, and creation, though could add more operations in the future.
The core workflow—check health, view demos and releases, add new demos—is covered. Missing update/delete operations, but this is intentionally delegated to the admin web interface, so no critical dead ends.