KVT is a git-backed knowledge vault service for OKF-style markdown bundles. It keeps the canonical knowledge in plain markdown files, maintains a derived SQLite search index, commits every write to git, and exposes the vault through REST and MCP.
- Stores concepts as bundle-relative
.mdfiles with YAML frontmatter. - Initializes and maintains a vault-local git repo.
- Regenerates service-owned
index.mdfiles. - Validates documents against optional
_ontology.yamlrules. - Indexes documents in SQLite with FTS5, links, frontmatter fields, and optional vector state.
- Serves REST endpoints and MCP tools for search, read, write, edit, delete, validate, history, and push operations.
- Uses optimistic concurrency through content hashes (
base_hash) so stale writes are rejected with the current content.
Markdown files are the source of truth. .kvt/index.db and other
runtime state are derived artifacts.
- Go 1.25+
- Git
- Optional: Docker and Docker Compose
- Optional: external embedding/rerank services for vector search and LLM reranking
Build the CLI:
go build -o kvt ./cmd/kvtInitialize a vault:
mkdir -p ./vault
./kvt init --vault ./vault --defaultsStart the service:
./kvt serve --vault ./vault --addr :8200Write a concept:
curl -sS http://localhost:8200/concepts \
-H 'content-type: application/json' \
-d '{
"path": "notes/hello.md",
"content": "---\ntype: Note\ntitle: Hello\n---\nHello from KVT.\n",
"validation_mode": "advisory"
}'Read it back:
curl -sS http://localhost:8200/concepts/notes/hello.mdValidate the vault:
./kvt validate --vault ./vaultA typical vault looks like this:
vault/
.git/
.kvt/
config.yaml
index.db
_ontology.yaml
_howto.md
index.md
notes/
hello.md
index.md
Important files:
_ontology.yaml: optional type and frontmatter validation rules._howto.md: optional vault-specific house rules surfaced throughkvt_howto, the MCP resource, and the MCP prompt.index.md: service-owned directory listing. Do not hand-edit it..kvt/config.yaml: runtime config generated byinit --defaults..kvt/index.db: derived SQLite index.
Concept paths must be bundle-relative, lowercase, forward-slash
separated markdown paths such as people/alice.md. Paths may not use
absolute paths, .., spaces, or unsafe segments.
_ontology.yaml defines allowed concept types, required fields,
optional fields, shallow field constraints, and path rules.
Example:
types:
Person:
required: [title, description]
optional: [email, github]
System:
required: [title, description]
Incident:
required: [title, severity, status]
fields:
severity: {enum: [low, medium, high, critical]}
status: {enum: [open, investigating, resolved]}
affects: {ref: System}
rules:
- path: people/**
type: Person
- path: systems/**
type: System
unknown_types: warnWrites default to strict validation. Use validation_mode: "advisory"
when you want KVT to commit a document and return warnings instead of
rejecting advisory issues.
kvt init --vault <path> --defaults
kvt serve --vault <path> [--config <path>] [--addr :8200]
kvt reindex --vault <path> [--config <path>]
kvt validate --vault <path> [--config <path>]
kvt push --vault <path> [--config <path>] [--remote origin] [--branch main]
kvt version--vault can also be supplied by KVT_VAULT.
All responses are JSON. If auth.api_keys is configured, pass
Authorization: Bearer <key>.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Health, git status, summary, push status |
GET |
/summary |
Counts, vector status, embedding backlog, push status |
POST |
/search |
Hybrid search |
POST |
/grep |
Exact indexed content lookup |
GET |
/concepts |
List concepts by type, path prefix, or field filter |
POST |
/concepts |
Create or replace a concept |
GET |
/concepts/{path} |
Read a concept |
PATCH |
/concepts/{path} |
Exact string edit |
DELETE |
/concepts/{path} |
Delete a concept |
GET |
/history/{path} |
Paginated file history and diffs |
GET |
/log |
Paginated vault git log |
GET |
/types |
Ontology schemas and per-type counts |
POST |
/validate |
Whole-vault validation |
POST |
/push |
One-shot git push |
List, grep, log, and history responses include cursors and obey
limits.max_response_chars.
GET /concepts/{path} supports 1-based inclusive line ranges:
curl -sS 'http://localhost:8200/concepts/notes/hello.md?start_line=1&end_line=10'Malformed, negative, or reversed line ranges return 400.
REST and MCP calls can include an optional request-scoped access
object to narrow which vault paths the operation may read or write:
{
"access": {
"read_globs": ["public/**"],
"write_globs": ["drafts/**"],
"deny_globs": ["secrets/**"]
}
}GET routes use repeated query params:
?read_glob=public/**&deny_glob=secrets/**
Missing access preserves normal unrestricted behavior. Present but
empty access denies read/write access. deny_globs wins over allow
globs. * and ? stay within one path segment; ** may cross path
separators. This is request sandboxing, not authenticated server-side
ACLs.
KVT registers fixed kvt_ tool names:
kvt_summarykvt_howtokvt_searchkvt_grepkvt_listkvt_readkvt_typeskvt_logkvt_historykvt_writekvt_editkvt_deletekvt_validate
There is intentionally no MCP push tool.
By default, server.mcp_transport is stdio. Set it to
streamable-http in .kvt/config.yaml to expose MCP at /mcp through
the same HTTP server. Streamable HTTP MCP is protected by the same
Bearer auth middleware as REST when auth.api_keys is configured.
kvt init --defaults writes .kvt/config.yaml with search, git,
server, and response-limit defaults:
search:
fusion: rrf
fts_weight: 0.5
vec_weight: 0.5
rerank: true
rerank_top_k: 10
git:
branch: main
remote_name: origin
push: "off"
debounce_interval: 5m
author_name: kvt
author_email: kvt@local
server:
http_port: 8200
mcp_transport: stdio
limits:
max_response_chars: 16000Optional sections can be added when needed:
embedder:
type: openai-compatible # or ollama
base_url: http://localhost:11434
model: nomic-embed-text
dimensions: 768
api_key_env: EMBEDDER_API_KEY
llm:
base_url: http://localhost:8000/v1
model: reranker-model
api_key_env: LLM_API_KEY
search:
fusion: rrf
fts_weight: 0.5
vec_weight: 0.5
rerank: true
rerank_top_k: 10
git:
branch: main
remote_name: origin
push: off # off, on_change, or debounced
debounce_interval: 5m
author_name: kvt
author_email: kvt@local
server:
http_port: 8200
mcp_transport: stdio # or streamable-http
auth:
api_keys: ["local-secret"]Vector search degrades to FTS-only when sqlite-vec or the configured embedder is unavailable. Health and summary responses report vector status and embedding backlog.
Initialize the mounted vault first:
mkdir -p vault
docker compose run --rm kvt init --vault /workspace --defaultsThen start the service:
docker compose up --buildThe compose file mounts ./vault at /workspace and publishes port
8200.
Run the main verification commands:
go test ./...
go vet ./...
go build ./cmd/kvtSmoke-test init with the built binary:
tmp=$(mktemp -d)
./kvt init --vault "$tmp" --defaults
rm -f ./kvtVISION.md describes the broader product direction. The current
implementation is documented in
docs/verification/full-scope-audit.md, including scoped deviations.
Notable current limits:
kvt_summaryis operational rather than a pretty tree view.- Tags and arbitrary frontmatter values are stored through
kb_fields, not dedicatedkb_docs.tagscolumns. kvt reindexperforms an in-place forced reconcile rather than a temp database atomic swap.kvt_listsupports type, path-prefix, and generic field filters; arbitraryorder_byand type-aware sorting are not implemented.kvt_grepis FTS-backed exact indexed lookup, not raw-file regex grep.init --defaultsis deterministic; there is no interactive questionnaire.