vellum

mcp
Security Audit
Fail
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Fail
  • new Function() — Dynamic code execution via Function constructor in design/support.js
  • exec() — Shell command execution in design/support.js
  • network request — Outbound network request in design/support.js
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Lightweight self-hosted MCP server over a folder of markdown files. One Go binary, embedded React SPA, no DB.

README.md

vellum

a calm window into a folder of markdown

Release
CI
Go
License: MIT
GHCR
Website

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.
  • Sharing is a link, not an export. Any note can become a read-only
    URL for someone who has no account; it always shows the current
    version and dies the moment you revoke it. Off by default.

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:

Vellum notes surfaced as MCP resources in the client's resource picker

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
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 <<EOF
AUTH_ENABLED=true
VELLUM_CLIENT_SECRET=$(openssl rand -hex 32)
EOF
docker compose up -d
curl http://localhost:8080/healthz

Open http://localhost:8080, paste the client secret — that's your vault.

Everything else is tunable from the same .env: pin the image with
VELLUM_IMAGE=ghcr.io/freema/vellum:1.9.0 (recommended for production —
latest moves), change the host port with VELLUM_PORT, plus any
variable from the configuration table.

No Docker

go install github.com/freema/vellum/cmd/vellum@latest
vellum                # HTTP API + MCP on :8080, vault in ./vault
vellum -mcp-stdio     # MCP over stdio for local clients

A plain Go build serves the API and MCP but not the web UI — the SPA is
embedded only in the Docker image and in task build-full builds
(-tags embedspa, needs Node; see Development).

Connect Claude

The connector URL must end with /mcp:

claude mcp add --transport http vellum https://your-host/mcp

Claude runs the OAuth flow: a browser window opens vellum's consent screen,
you enter your VELLUM_CLIENT_SECRET and approve (authorization code +
PKCE; vellum is its own OAuth issuer — no external identity provider, no
calls out). The secret is what stops anyone else who can reach the server
from approving a connection of their own. The same works for a
claude.ai custom connector — either
leave the OAuth fields empty and enter the secret on the consent screen, or
enter vellum as client ID and your secret there and approve with one click.

Local, no Docker: vellum -mcp-stdio serves MCP over stdio.

MCP tools

list_notes, read_note, write_note, patch_note, append_to_note,
prepend_to_note, delete_note, move_note, search_notes, list_tags,
add_tags, remove_tags, get_backlinks, set_status, list_tasks —
plus, with VELLUM_CURATOR=on: suggest_location, suggest_tags,
suggest_links, find_untagged, find_orphans, find_inbox_stale, and
with VELLUM_SHARING=on: share_note, unshare_note.

Writes are conflict-safe: read_note returns a content hash, write tools
accept expected_hash and fail on mismatch instead of clobbering.

Every tool carries MCP annotations (readOnlyHint, destructiveHint,
idempotentHint), and the handshake ships short server instructions with
the vault conventions, so agents behave well out of the box. The full
tool/resource reference lives in docs/mcp.md.

MCP resources

Notes are also exposed as resources: vellum://note/{path} serves the raw
markdown (text/markdown), so clients with a resource picker can attach a
note as context without a tool call. Clients can resources/subscribe to a
note URI and receive notifications/resources/updated whenever that note
changes — regardless of whether the change came through MCP or the web
editor.

Web workspace

The embedded UI is a three-pane workspace (tree, list, editor) that stays
in sync with the vault on its own: MCP writes appear without a reload, a
deleted note switches to a designed 410 state, and a deploy restart shows
a reconnect overlay instead of a dead page. Filters (folder, tags,
type/status) live in the URL so views are shareable and refresh-safe;
theme and editor mode persist locally. Details — URL scheme, storage
keys, autosave/conflict/flush semantics: docs/workspace.md.

Sharing a note

Any note can become a link somebody else can open without an account —
for handing over a document your agent just wrote, without exporting it
or giving anyone access to the vault:

VELLUM_SHARING=on     # links + the share_note/unshare_note MCP tools
VELLUM_SHARING=ui     # links, but only you can create them
VELLUM_SHARING=off    # default

Then Share in the note header hands you
https://your-host/s/<token>. The link is a pointer, not a copy: the
reader always sees the current note, an edit shows up immediately, and
revoking takes effect on the next request. It follows the note when you
rename or move it, and dies with it when you delete it. Frontmatter is
never served, and the page is one document — no tree, no search, no way
into the rest of the vault.

Links live in <vault>/.vellum/shares.json, so they survive a restart
(unlike the in-memory OAuth tokens) and travel with your vault backup.
Full reference — lifetimes, headers, rate limits, what a leaked link
costs: docs/sharing.md.

Vault structure, tasks, curator

  • Structure: an empty vault is initialized with inbox/, projects/,
    archive/ (disable with VELLUM_INIT_STRUCTURE=false). A write_note
    without a path files the note into the inbox under a title slug.
    Pointing vellum at an existing vault changes nothing.
  • Tasks: a note with type: task and status: backlog | in-progress | done in the frontmatter. set_status edits just those lines; the UI and
    list_tasks filter by state and project. Notes without a type are
    knowledge.
  • Curator (VELLUM_CURATOR=on, default off): deterministic context
    tools — folder suggestions by tag overlap, tag vocabulary for a note,
    link candidates, untagged/orphaned/stale-inbox lists. No API keys, no
    LLM calls from the server, moves stay behind the regular move_note.

Configuration

Variable Default Description
PORT 8080 HTTP listen port
VELLUM_VAULT_PATH ./vault Vault directory (Docker: /vault)
AUTH_ENABLED false OAuth 2.1 with a single client secret — required for anything public
VELLUM_CLIENT_ID vellum OAuth client id
VELLUM_CLIENT_SECRET — The access key (openssl rand -hex 32, min 32 chars)
VELLUM_ISSUER_URL http://localhost:PORT Public HTTPS URL when behind a proxy — without it OAuth metadata advertises localhost and clients fail
TRUST_PROXY false Set 1 behind Caddy/Traefik so rate limiting sees real client IPs
CORS_ORIGINS claude.ai, claude.com Origins receiving CORS headers (localhost always allowed)
VELLUM_ALLOWED_ORIGINS claude.ai, claude.com Browser origins allowed on /mcp + /api (same-origin and localhost always pass)
VELLUM_REDIRECT_URIS any Optional exact OAuth redirect allowlist
VELLUM_INIT_STRUCTURE true Create inbox/projects/archive in an empty vault
VELLUM_INBOX_DIR / VELLUM_PROJECTS_DIR / VELLUM_ARCHIVE_DIR inbox/projects/archive Conventional directory names
VELLUM_CURATOR off Enable the suggest_*/find_* context tools
VELLUM_SHARING off Public read-only note links: off / ui (you mint them) / on (agents may too)

Deployment

vellum stays on the internal network; only your reverse proxy is exposed.
Set VELLUM_ISSUER_URL=https://<domain>, TRUST_PROXY=1 and terminate TLS
in the proxy. Details and a smoke-test checklist: 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):

    AUTH_ENABLED=true
    VELLUM_CLIENT_SECRET=<openssl rand -hex 32>
    VELLUM_ISSUER_URL=https://<your-public-url>   # 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://<your-public-url>/mcp (see
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:

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, docs/threat-model.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). 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 and
SECURITY.md explain the boundaries contributions must keep.

License

MIT

Reviews (0)

No results found