Skip to main content
Glama
README.md
# 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

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count4/5

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.

Completeness4/5

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.

Maintenance

ActivitySlowing
ResponsivenessSyncing