Install
$ agentstack add mcp-freema-vellum ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
vellum
> a calm window into a folder of markdown
[](https://github.com/freema/vellum/releases) [](https://github.com/freema/vellum/actions/workflows/ci.yml) [](go.mod) [](LICENSE) [](https://github.com/freema/vellum/pkgs/container/vellum)
A lightweight, self-hosted MCP server over a folder of markdown files. One static Go binary with an embedded web UI, one ~24 MB Docker image, ~5 MB idle RAM. docker compose up -d and you're done.
Deliberately light — the smallest stack that just works, for one person or a small team:
- Your data is flat markdown. Portable, readable in any editor, git or
Obsidian. No lock-in, no database, no embeddings.
- vellum never calls an LLM itself. The optional curator tools prepare
context; the agent on your side decides.
- The vault layer is dumb and deterministic. Everything is a file;
config is environment variables.
- Structure is a feature.
inbox/,projects/,archive/— not a
pile of files. Unfiled notes land in the inbox.
- Small MCP tool surface (15 tools) — less agent context burned on
tool definitions.
Every note is also a first-class MCP resource — vellum://note/{path} — so your agent can attach a note as context straight from its resource picker, no tool call needed:
Quick start
Pre-built multi-arch images (amd64 + arm64) are published to GitHub Container Registry on every release:
docker pull ghcr.io/freema/vellum:latest
Try it in one command
mkdir -p vault
docker run --rm -p 8080:8080 -v "$PWD/vault:/vault" ghcr.io/freema/vellum
Open http://localhost:8080 — that's your vault, served straight from ./vault. Auth is off by default, which is fine on localhost; enable it before exposing anything (see below).
> Linux bind mounts: the container runs as a non-root user > (uid 65532, distroless), so the mounted directory must be writable by > it: sudo chown 65532 vault. Docker Desktop on macOS/Windows handles > this for you.
Docker Compose (recommended)
Create a docker-compose.yml — this is [the same file](docker-compose.yml) the repo ships (curl -fsSLO https://raw.githubusercontent.com/freema/vellum/main/docker-compose.yml grabs it if you'd rather not paste):
services:
vellum:
image: ${VELLUM_IMAGE:-ghcr.io/freema/vellum:latest}
container_name: vellum
restart: unless-stopped
ports:
- "${VELLUM_PORT:-8080}:8080"
env_file:
- path: .env
required: false
environment:
PORT: "8080"
VELLUM_VAULT_PATH: /vault
volumes:
- ./vault:/vault
# hardening: the binary only ever writes into the mounted vault
read_only: true
security_opt:
- no-new-privileges
deploy:
resources:
limits:
memory: 256M
Generate a secret and start:
cat > .env `, `TRUST_PROXY=1` and terminate TLS
in the proxy. Details and a smoke-test checklist: [docs/deployment.md](docs/deployment.md).
### One-click PaaS (Railway / Render / Fly.io)
The easiest path if you don't want to run a VPS: deploy the published image
on a platform that gives you TLS and a public URL for free. vellum honours
the `PORT` the platform injects, so the only three things that matter are the
**same everywhere**:
1. **Image**: `ghcr.io/freema/vellum:1.9.0` (pin a version, not `:latest`).
2. **A persistent volume mounted at `/vault`** — this is not optional. vellum
stores every note as a file; without a persistent disk your vault is wiped
on the next redeploy. (Serverless hosts like Vercel/Netlify/Cloudflare
Pages have no persistent writable disk and can't run vellum — use one of
the platforms below.)
3. **Env** (from the [configuration table](#configuration)):
```sh
AUTH_ENABLED=true
VELLUM_CLIENT_SECRET=
VELLUM_ISSUER_URL=https:// # the URL the platform gives you
TRUST_PROXY=1 # you're behind the platform's proxy
```
Set `VELLUM_ISSUER_URL` once you know the assigned URL, then redeploy — OAuth
metadata must advertise the real HTTPS host or Claude fails after the consent
step.
- **Railway**: New Project → Deploy from Docker image →
`ghcr.io/freema/vellum:1.9.0`. Add a Volume mounted at `/vault`, set the env
above (Railway provides `PORT` and a `*.up.railway.app` domain).
- **Render**: New → Web Service → *Deploy an existing image* →
`ghcr.io/freema/vellum:1.9.0`. Add a Disk mounted at `/vault`, set the env.
- **Fly.io**: `fly launch --image ghcr.io/freema/vellum:1.9.0`, then
`fly volume create vault` and mount it at `/vault` in `fly.toml`; set the env
with `fly secrets set VELLUM_CLIENT_SECRET=… VELLUM_ISSUER_URL=… TRUST_PROXY=1 AUTH_ENABLED=true`.
Then connect Claude to `https:///mcp` (see
[Connect Claude](#connect-claude)).
**Coolify / Traefik**: point Coolify at the repo or image
`ghcr.io/freema/vellum` (pin a version, e.g. `:1.9.0`, not `:latest`),
mount a volume at `/vault`, set the env from the table above. Running raw
compose behind Traefik, uncomment the labels in `docker-compose.yml`:
```yaml
labels:
- traefik.enable=true
- traefik.http.routers.vellum.rule=Host(`vellum.example.com`)
- traefik.http.services.vellum.loadbalancer.server.port=8080
Caddy is two lines:
vellum.example.com {
reverse_proxy vellum:8080
}
Security posture (read-only container, threat model, what is never logged): [SECURITY.md](SECURITY.md), [docs/threat-model.md](docs/threat-model.md), [docs/logging.md](docs/logging.md).
Development
Requires Go 1.25+, Task, and Node 22 for the SPA.
task build # Go binary without the SPA (no node needed)
task build-full # SPA + Go binary with the UI embedded
task test # go test ./...
task lint # golangci-lint
task e2e # compose stack with the fixture vault (docs/e2e.md)
Design decisions kept deliberately simple
- Search = ranked RAM scan, not bleve. The metadata index narrows by
tag/dir, content is matched from an in-memory cache invalidated precisely by the index (modTime+size), and results are ranked (title > tag > path > body, word starts beat mid-word). Matching is case-, diacritics- and typo-insensitive — "preklapy" finds "překlepy". A query over a 2 000-note vault takes ~0.4 ms warm ([docs/search.md](docs/search.md)). Zero heavy dependencies, no index to corrupt, no warm-up. The Searcher interface exists so bleve can slot in if it ever hurts.
- Plain CSS custom properties, not Tailwind. The design system is a
fixed token sheet (DESIGN.md); custom properties map to it 1:1 with no build-time dependency.
- Opaque in-memory tokens, not JWTs. Tokens die with the process and
clients silently re-authorize; no signing keys to manage.
- No database, ever (v1). Tags, backlinks and task states live in an
in-RAM index rebuilt in ~50 ms at startup.
Why vellum and not …?
- Obsidian + plugins — vellum doesn't replace your editor; it happily
serves the same vault folder to your agent while you keep editing anywhere. No sync, no plugin sandbox, no Electron on the server.
- Notion/Anytype-style apps — those own your data. vellum's "database"
is ls and grep-able markdown.
- Heavier MCP note servers — vellum ships one 24 MB container, no
vector DB, no API keys, and burns minimal agent context (15 tools).
Author
Created by Tomáš Grasl (@freema).
Issues and PRs welcome — the [threat model](docs/threat-model.md) and [SECURITY.md](SECURITY.md) explain the boundaries contributions must keep.
License
[MIT](LICENSE)
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: freema
- Source: freema/vellum
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.