An in-process validation & search engine for markdown documents — schema inference, frontmatter validation, and local semantic search.
Why mdvs? • What it does • Installation • Documentation • Example Vault
mdvs is useful when you have a markdown corpus with structured frontmatter. Some common cases:
- Obsidian vaults — typed-frontmatter notes you want to keep validated and searchable, all local.
- Knowledge bases maintained with LLM agents — mdvs is the typed database the agent reads from (hybrid search with SQL
--wherefilters) and validates against (mdvs checkafter each turn). Everything via--output json. - Docs-as-code repos (Hugo, MkDocs, Astro) — frontmatter consistency enforced in CI; JSON Schema export for downstream tools.
- Infers a typed schema from your existing files. No config to write — point it at a directory and it figures out which fields belong where.
- Validates frontmatter. Catches wrong types, missing required fields, and disallowed locations. Path-scoped rules:
rolecan be required inteam/but not inblog/. - Multi-format frontmatter. YAML (
---), TOML (+++), or JSON ({...}), auto-detected per file. Mix freely within one vault. - Hybrid search. Vector similarity + BM25 full-text + RRF fusion. SQL
--wherefilters on typed frontmatter:--where "status = 'published' AND date > '2026-05-01'". - JSON Schema interop.
mdvs export-jsonschemaemits a JSON Schema 2020-12 document;mdvs init --from-jsonschemaimports one. - Runs entirely in-process. Local files, single binary. No API keys, no vector-DB cluster, no GPU.
- Incremental builds. Only changed files are re-embedded; the Lance write itself is also incremental (delete + append + optimize, not a full overwrite). If nothing changed, the model isn't loaded and the index isn't rewritten.
- Auto-pipeline.
searchauto-builds the index if needed.buildauto-updates the schema before embedding. The whole chain is cheap on unchanged corpora — so a baremdvs search "query"does everything in one shot. - Agent-callable and CI-ready.
--output jsonon every command. Deterministic exit codes (0= success,1= violations,2= error).
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/edochi/mdvs/releases/latest/download/mdvs-installer.sh | shcargo install mdvsgit clone https://github.com/edochi/mdvs.git
cd mdvs
cargo install --path .Markdown files can have a block at the top called frontmatter — structured fields that describe the document. mdvs accepts YAML (---), TOML (+++), or JSON ({...}); the format is auto-detected per file, so mixed-format vaults work transparently.
---
title: Rust Tips
tags: [rust, programming]
draft: false
---
# Rust Tips
Your content here...title, tags, and draft are frontmatter fields. mdvs treats them as a typed database — and your directory structure is part of the schema.
Consider a simple knowledge base:
notes/
├── blog/
│ ├── rust-tips.md ← title, tags, draft
│ └── half-baked-idea.md ← title, draft
├── team/
│ ├── alice.md ← title, role, email
│ └── bob.md ← title, role
└── meetings/
└── weekly.md ← title, date, attendees
Different directories, different fields. mdvs sees this.
cd notes/
mdvs initmdvs scans every file, extracts frontmatter, and infers which fields belong where. draft is a Boolean allowed in blog/. role is a String required in team/. date is a Date (RFC 3339) allowed in meetings/. The directory structure is the schema.
Two new files appear — both without role:
notes/
├── blog/
│ └── new-post.md ← title, draft (no role)
├── team/
│ └── charlie.md ← title (no role)
└── ...
mdvs checkChecked 7 files — 1 violation(s)
┌ role ────────────────────┬─────────────────────────────────────────┐
│ kind │ Missing required │
│ rule │ required in ["team/**"] │
│ files │ team/charlie.md │
└──────────────────────────┴─────────────────────────────────────────┘
charlie.md is missing role — but new-post.md isn't flagged. mdvs knows role belongs in team/, not in blog/.
This is especially useful when an LLM agent is doing the writing. Over a long session an agent will drift — a misnamed field here, an accidentally-stringified boolean there — and running mdvs check after each turn catches the drift before it compounds.
mdvs search "how to get in touch"Searched "how to get in touch" — 3 hits
┌ #1 ──────────────────────┬─────────────────────────────────────────┐
│ file │ team/alice.md │
│ score │ 0.612 │
│ lines │ 5-8 │
│ text │ Alice leads the backend team. Reach her │
│ │ at alice@example.com or on Slack. │
└──────────────────────────┴─────────────────────────────────────────┘
alice.md doesn't contain "get in touch" — mdvs finds it by meaning, not keywords. Filter with SQL on frontmatter or on path:
mdvs search "rust" --where "draft = false"
mdvs search "meeting notes" --where "date > '2026-05-01'"
mdvs search "incident" --where "filepath LIKE 'logs/%'" # restrict by directory
mdvs search "review" --where "filepath LIKE '%-postmortem.md'" # match by filename suffixThe typed schema is what makes frontmatter filters work — mdvs knows tags is Array(String), so --where "tags = 'rust'" is auto-rewritten to array_has(data.tags, 'rust') (the search output prints a one-line "Note" showing the rewrite so it's never magic). =, !=, IN, and NOT IN all do element-containment against array fields. The always-present filepath column lets you filter by path with standard LIKE patterns; its last component is the filename.
Try it on your own files:
cargo install mdvs cd your-notes/ mdvs init mdvs search "your query"Or explore the repo's example_kb/ — 43 files across 8 directories with type widening, nullable fields, nested objects, and deliberate edge cases.
Every command supports --output json and returns deterministic exit codes (0 = success, 1 = violations, 2 = error). No SDK or daemon to manage — a coding agent calls mdvs the way a shell script would.
# An agent checks its own writes:
mdvs check --output json | jq '.violations[] | select(.kind == "MissingRequired")'
# An agent queries by metadata + meaning together:
mdvs search "incident postmortem" \
--where "status = 'published' AND severity = 'high'" \
--output json | jq '.hits[].filename'
# An agent exports the schema to feed into a structured-output generator:
mdvs export-jsonschema --format json| Command | Description |
|---|---|
init |
Scan files, infer schema, write mdvs.toml (or import via --from-jsonschema) |
check |
Validate frontmatter against schema (optionally --jsonschema to override) |
update |
Re-scan and update field definitions |
build |
Validate + embed + write search index |
search |
Semantic search with optional SQL filtering |
info |
Show config and index status |
clean |
Delete search index |
export-jsonschema |
Translate mdvs.toml fields into a JSON Schema 2020-12 document |
skill |
Print the agent skill file to stdout (for harnesses that load it as a tool description) |
- Rust — mdvs is written in Rust; the CLI is a single static binary.
- LanceDB — backs storage and search. Cosine vector search, BM25 full-text, and RRF hybrid all run natively against the Lance dataset.
- Model2Vec — static embedding models; the default is
potion-multilingual-128M(~480 MB, 101 languages, CPU-only, no GPU). Smaller models likepotion-base-8M(~60 MB) are available via--set-model. jsonschema— JSON Schema 2020-12 validator. mdvs translates yourmdvs.tomlinto a canonical JSON Schema document and validates frontmatter values through per-field validators compiled from it.pulldown-cmark— markdown parsing; used to extract plain text from each chunk before embedding.text-splitter— semantic-aware chunker that splits the markdown body along heading and paragraph boundaries.gray_matter— YAML and TOML frontmatter extraction. JSON frontmatter is parsed natively viaserde_jsonto handle Hugo's bare-braces convention.
Full documentation at edochi.github.io/mdvs.
