An MCP server that gives AI agents a standardized Open Knowledge Format (OKF) backend: a knowledge base of plain Markdown files with YAML frontmatter, indexed into a link graph and exposed through MCP resources and tools for search, traversal, validation, and authoring.
The knowledge base itself is just a directory of Markdown ("a bundle"). Humans can browse and edit it with any editor — including opening it directly as an Obsidian vault — while agents work through the MCP server. Obsidian is never required: the format is pure OKF v0.1 Markdown, and any .obsidian/ directory is ignored by the indexer.
- No database, no embeddings; the only network calls are for optional read-only remote bundles you explicitly configure.
- Permissive by design (spec §9): malformed documents are reported, valid concepts keep serving.
- The write path keeps
index.mdnavigation pages and thelog.mdhistory up to date, so the human view stays browsable as agents write.
npm install
npm test
npm run dev -- --bundle acme=examples/acme inspectStart the stdio MCP server (the default command):
npm run dev -- --bundle acme=examples/acme # read-only
npm run dev -- --bundle acme=examples/acme --writable # + authoring toolsOr build and run the compiled binary:
npm run build
node dist/cli.js --bundle /path/to/your/bundleThe package is published to npm, so the easiest setup runs it through npx:
{
"mcpServers": {
"okf": {
"command": "npx",
"args": [
"-y", "@copperbox/okf-mcp",
"--bundle", "brain=/absolute/path/to/your/bundle",
"--writable"
]
}
}
}Alternatively, point at a local checkout you built yourself (npm run build):
{
"mcpServers": {
"okf": {
"command": "node",
"args": [
"/absolute/path/to/okf-mcp/dist/cli.js",
"--bundle", "brain=/absolute/path/to/your/bundle",
"--writable"
]
}
}
}--bundle accepts path or id=path and is repeatable. Omit --writable for a read-only server.
The server is deliberately not opinionated about when knowledge gets captured: its built-in instructions teach connected agents the OKF conventions and the write flow (suggest_concept_path → write_concept), but nothing tells an agent to record what it learns as a side effect of ordinary work. Capture policy belongs in your agent's own configuration — CLAUDE.md, AGENTS.md, or the system prompt, whichever your client reads.
The server also performs no git sync. If the bundle is shared — mounted from a local clone of a team repository — it is only as fresh as the clone's last git pull, and concepts written through write_concept reach teammates only after an out-of-band commit and push. Keeping a shared bundle in sync is your (or your agent's) responsibility, so it belongs in the same standing instructions.
If you want the ambient "brain grows while you work" behavior, a standing instruction like this is enough to get started — copy it into your agent config and adjust to taste:
## Knowledge capture (OKF brain)
This project keeps a persistent knowledge base (the "brain") behind the `okf` MCP server.
- Before starting non-trivial work, check the brain: orient with `graph_summary`, then
`search_concepts` for anything related to the task, and treat what you find as prior
context.
- When you learn something durable — a decision and its rationale, a gotcha, how a
system actually works, a convention worth keeping — record it before finishing:
call `suggest_concept_path` to pick a placement, then `write_concept`. Prefer
updating an existing concept over creating a near-duplicate.
- Keep concepts small and linked: one idea per concept, bundle-absolute markdown
links (`/tables/orders.md`) to related concepts, and reuse existing types and tags.
- Don't record ephemera (task status, one-off debugging detail) — the brain is for
knowledge that should still be true next month.
- If the brain is shared (a clone of a team repo), the server never syncs git for you.
Before relying on it, make sure the clone is current — run `git pull` in the bundle
repo, then call `reload_bundles` so the index picks up the changes. For
`--remote-bundle` mounts, calling `reload_bundles` refetches.
- After writing durable knowledge to a shared brain, commit and push it if you're
authorized to, or remind the user to — until then the new knowledge is invisible
to teammates.This works from a standing start: point --bundle at an empty directory with --writable and the first write_concept creates the folder structure, navigation indexes, and log.
--bundle and --remote-bundle are repeatable, so one server can mount a shared org-wide brain next to the project's own bundle. Mount the org brain from a local clone when agents should write to it:
{
"mcpServers": {
"okf": {
"command": "npx",
"args": [
"-y", "@copperbox/okf-mcp",
"--bundle", "org=/absolute/path/to/org-brain-clone",
"--bundle", "project=/absolute/path/to/this-repo/brain",
"--writable"
]
}
}
}If consuming the org brain is enough, swap its mount for a read-only GitHub tree — no clone to keep fresh, and reload_bundles refetches it: "--remote-bundle", "org=https://github.com/your-org/brain/tree/main/bundle". Note that --writable is server-wide — every local --bundle it mounts becomes writable — so --remote-bundle is also the way to keep the org brain read-only for agents while the project bundle stays writable.
Two routing behaviors make this workable:
- Aggregate read tools —
search_concepts,list_concepts,list_types,list_tags,graph_summary,validate_bundle— cover all bundles when thebundleparameter is omitted, so one search spans both brains. - Per-concept and write tools (
get_concept,get_neighbors,write_concept, …) require an explicitbundleonce more than one is mounted, so a write always names its destination.
Split knowledge by scope: standards, environment architecture, and cross-repo system maps belong in the org bundle; decisions and gotchas specific to one repo belong in that repo's project bundle. Cross-bundle markdown links are not part of OKF (§5 links resolve within a single bundle, so a link into another bundle just indexes as broken) — to reference an org concept from a project concept, cite it in a # Citations section (spec §8) using the org bundle's canonical URL; when a real graph edge matters, add a small stub concept under references/ in the project bundle that mirrors the org concept, link to the stub, and let the stub's citation point at the source. When project knowledge turns out to be org-wide, promote_concept moves it into the org bundle and leaves exactly such a citation stub behind at the old path, so the project bundle's inbound links keep resolving.
Append routing guidance to the CLAUDE.md snippet above:
- Two brains are mounted: `org` (cross-project standards, environment architecture,
system maps) and `project` (this repo's decisions, gotchas, conventions). Before
starting work, search both — omit the `bundle` parameter so `search_concepts` and
`graph_summary` cover all bundles, or query each in turn.
- Route writes by scope: knowledge specific to this repo goes to the `project`
bundle. Knowledge that holds across projects — standards, shared infrastructure,
org-wide architecture — goes to the `org` bundle; if the org brain is mounted
read-only, record it in the project bundle and flag it for promotion.
- Never write markdown links from one bundle into another — they index as broken.
Reference org concepts from project concepts via a `# Citations` entry using the
org bundle's canonical URL, or through a `references/` stub concept when a graph
edge is needed.A common layout keeps several bundles as sibling subdirectories of one root — for example a repo opened as a single Obsidian vault:
knowledge/ ← repo root, opened as an Obsidian vault
├── AGENTS.md ← belongs to no bundle
├── acme/ ← bundle "acme"
└── ops/ ← bundle "ops"
Instead of repeating --bundle for each subdirectory, mount them all with one flag:
okf-mcp --colocated-bundles /path/to/knowledgeEvery immediate subdirectory of the root that contains at least one markdown file is mounted as its own bundle, with the folder name as its bundle id (the same default-id rule --bundle <path> uses). Dot directories (.obsidian, .git) are skipped, and loose files at the root (README.md, AGENTS.md) belong to no bundle. The flag is repeatable and combines with --bundle / --remote-bundle; a discovered id colliding with another mount is a startup error naming the colocated root. Beyond saving flags, --colocated-bundles declares the sibling layout (colocatedRoot on each discovered bundle's config), so features that reason about colocated siblings can tell them apart from independently mounted bundles. Relative ../sibling/... links between colocated bundles derive cross-bundle graph edges, resolve citations, are checked by validate, and are rewritten to the sibling's canonical URL by pack.
When the root holds many bundles but the current project only needs a few, --only <folder,folder,...> is the escape hatch for scoped mounting:
okf-mcp --colocated-bundles /path/to/knowledge --only acme,opsOnly the named subfolders are mounted; everything else in the root is ignored entirely — not discovered, not listed — which keeps startup light and stops irrelevant bundles from diluting search results, type/tag vocabularies, and graph_summary sweeps. A name that doesn't exist as a subdirectory of the root (or exists but contains no markdown) is a startup error rather than a silent skip, and passing --only without --colocated-bundles is an error too.
When the root is published as one repo, every bundle's canonical URL is mechanical — the repo's tree URL plus the folder name — so one flag declares them all for cross-bundle awareness:
okf-mcp --colocated-bundles /path/to/knowledge \
--canonical-url https://github.com/acme/knowledge/tree/mainEach colocated bundle derives canonicalUrl = <rootUrl>/<folder> (here …/tree/main/acme, …/tree/main/ops), and the derived URLs get the same tree/blob/raw prefix expansion as explicitly declared ones. A bare URL works when exactly one colocated root is configured; with several, name the root: --canonical-url /path/to/knowledge=<url>. An explicit per-bundle --canonical-url id=<url> still overrides the derived value. Non-GitHub root URLs work too — the folder name is appended to the literal prefix. Consumers can still mount an individual bundle straight from its subdirectory tree URL (--remote-bundle acme=https://github.com/acme/knowledge/tree/main/acme) — or mount the whole published root by one URL with --colocated-remote-bundles.
A colocated root may hold many bundles a given project never touches, so the MCP server (mcp, the default command) mounts colocated bundles lazily: at startup each subdirectory costs only its discovery — the folder name plus a frontmatter-only read of its root index.md for the description — and a bundle is parsed and indexed the first time any tool names it (bundle argument, okf:// resource read, reload_bundles <id>, …). The semantics, chosen so nothing is silently truncated:
list_bundleslists every bundle with aloadedmarker; unloaded ones carry their name and description so an agent can see what exists and choose what to hydrate.- No-arg sweeps (
search_concepts,list_concepts,list_types,list_tags,graph_summary,validate_bundle) cover loaded bundles only, and the tool result carries a note naming the discovered bundles that were excluded — irrelevant bundles stop polluting results, without the truncation reading as complete coverage. resources/listrepresents an unloaded bundle by its rootindex.mdalone (with the discovered description); reading it loads the bundle, after which its documents list individually.- Cross-bundle derivation sees loaded siblings only: a
../sibling/...link into an unloaded bundle derives no edge and its citation classifiesmissinguntil the sibling loads (any access to it makes the edges appear);validate's dangling-link warnings likewise stay silent for unloaded siblings. - No-arg
reload_bundlesreloads loaded bundles only (an unloaded bundle has no stale index); naming an unloaded bundle loads it.--watchwatches loaded bundles and starts watching a lazy bundle the moment it hydrates. --onlycomposes: filtered-out subfolders are not even discovered.
One-shot CLI commands (inspect, validate, search, …) sweep every bundle by design, so they load colocated bundles eagerly as before. Remote mounts are always eager — a remote root is one fetch, and its folder names are unknown until fetched.
If the colocated root holds an AGENTS.md (exact name), its content is appended to the MCP server instructions under a Bundle guide (from AGENTS.md): delimiter, so every session starts knowing which bundles exist and which matter for what kind of work — and passes explicit bundle arguments instead of sweeping everything. Write it as a short registry for an agent deciding where to look: a line or two per bundle, what it covers, when to reach for it. It doubles as a readable vault-root note in Obsidian and travels with the repo.
Instructions load into the agent's context every session, so the guide is budgeted: past 4 000 characters the server logs a warning and injects a truncated guide with a pointer at get_bundle_guide (and the full file). Keep it lean.
The guide is also available on demand through the get_bundle_guide tool, which is registered whenever a colocated root (local or remote) is mounted — including mid-session, via tools/list_changed, when load_colocated_remote_bundles mounts the first one. It returns each root's AGENTS.md in full (local roots are read from disk per call, so external edits show up; remote roots return the guide fetched with the mount) plus every bundle's one-line description, for one root (root argument: the local path or remote URL) or all mounted roots when omitted. The base instructions tell agents to call it before exploring, so the guide stays reachable at any point in a session — not just at the initialize handshake.
OKF's third goal is exchanging knowledge across systems. You can index a bundle published in another repository without cloning it, straight from a public GitHub tree:
npm run dev -- \
--remote-bundle okf=https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf \
inspect--remote-bundle id=url is repeatable and takes a https://github.com/<owner>/<repo>/tree/<ref>/<path> URL (refs containing / are not supported), or a .tar.gz / .tgz / .zip archive detected by extension — any http(s) URL, or a local archive path. The same thing is available at runtime through the load_remote_bundle tool ({ id, url, include?, exclude?, canonicalUrl? }, glob filters over bundle-relative paths), which mutates only the in-memory index; list_remote_bundles lists what is loaded, with the tree or archive URL as the source.
A colocated root published as a single repo can be mounted whole — each subfolder becoming its own read-only bundle — instead of repeating --remote-bundle per subdirectory tree URL:
okf-mcp --colocated-remote-bundles https://github.com/acme/knowledge/tree/main inspectThe same discovery rules as local --colocated-bundles apply: every immediate subdirectory containing markdown mounts as a bundle with the folder name as its id (a collision with another mount is an error naming the root), dot directories are skipped, and loose root files belong to no bundle. The mounted bundles declare the root URL as their shared colocated root, so relative ../sibling/... links inside the published repo derive cross-bundle edges between the remote siblings — a repo mounted by URL stays as coherent as a local clone, with no rewrite step. Tree mounts derive each bundle's canonical URL as <treeUrl>/<folder> automatically (tree/blob/raw forms all match); .tar.gz/.tgz/.zip roots (any http(s) URL, or a local path; entries are partitioned by first path segment after the wrapper directory) have no per-file URLs, so they derive canonicals only from an explicit root canonicalUrl. The flag is repeatable, --only <folder,folder,...> restricts the mount to named subfolders exactly as for local roots, and the 500-file / 10 MiB ceilings apply across the whole root — not per bundle — so mounting a root is never more expensive than mounting one big bundle.
A root AGENTS.md travels with the mount: the CLI fetches it and appends it to the server instructions as a bundle guide, same as a local colocated root. The runtime counterpart is the load_colocated_remote_bundles tool ({ url, only?, include?, exclude?, canonicalUrl? }) — MCP instructions are fixed at initialization, so the tool returns the AGENTS.md content in its result (agentsGuide) instead, registers get_bundle_guide (announced via tools/list_changed) so the guide stays retrievable for the rest of the session, and the agent still receives the guide. Reloading any mounted bundle refetches the whole root, tracking subfolders that appeared or vanished upstream.
Remote bundles are strictly read-only and sandboxed:
- Only
.mdfiles are indexed (GitHub trees via the contents API;GITHUB_TOKENis used for rate limits when set, and never sent to non-GitHub hosts), bounded to 500 files / 10 MiB per bundle; archive downloads are additionally capped at 10 MiB compressed. - Archive entries with path traversal (
.., absolute paths) are rejected; a single top-level directory wrapping all files (as in GitHub source tarballs) is stripped; zip64 archives are not supported. - Remote content is parsed as markdown, never executed, and never written to disk.
- All authoring tools reject read-only bundles, and
regenerate_indexes/ theindexcommand skip them. reload_bundlesrefetches them, reporting the same added/removed/changed delta as local bundles.
OKF §5 deliberately has no cross-bundle link syntax, but the server knows every mounted bundle's canonical location and can derive cross-bundle relationships from spec-clean data. When a concept's §8 citation target, external link, or frontmatter resource URL points under another mounted bundle's canonical location, the graph tools record a derived kind: "cross-bundle" edge to that concept — read-only, no new syntax in documents.
Colocated bundles get the same treatment without any URL: an ordinary relative link like [orders](../acme/tables/orders.md) — which Obsidian resolves natively when the colocated root is opened as one vault — derives a kind: "cross-bundle" edge when its first path segment names a mounted sibling (a bundle declaring the same colocated root) and the remainder resolves to one of its concepts (.md optional, like body links). Citations through such a link classify as concept instead of missing, and validate warns when a ../ link points into a mounted sibling at a concept it does not have (dangling); links to unmounted folders or loose root files stay silent — colocation is declared, never inferred from disk.
promote_concept emits exactly such links: between colocated siblings the citation stub cites the promoted copy by relative path (e.g. ../../org/standards/naming.md from a stub one directory deep) instead of an okf:// URI — and prefers the relative form even when the target has a canonical URL, since the on-disk vault UX is the point of colocation and publishing rewrites relative links to canonical URLs at pack time. The stub's frontmatter resource stays the canonical/okf:// URI (spec §4.1 wants a parseable URI). Non-colocated promotions keep citing the canonical location.
- GitHub tree mounts get their canonical location automatically (the
tree,blob, andraw.githubusercontent.comforms of the tree URL all match). - Local clones and archives have no inherent URL: give them one with
--canonical-url id=<url>(orcanonicalUrlonload_remote_bundle), e.g. the GitHub tree URL of the shared bundle's published location, so citations to it resolve even when it is mounted from a local checkout. - Colocated bundles published as one repo need only a root-level
--canonical-url <rootUrl>: each bundle derives<rootUrl>/<folder>, and explicit per-bundle flags override. graph_summaryreportscrossBundleEdges;get_neighborsandfind_pathtraverse derived edges when called withcrossBundle: true(node IDs becomebundle:conceptand carry the target's bundle);export_graphwithcrossBundle: trueemits one namespaced multi-bundle graph with derived edges rendered dashed indot/mermaid.
A bundle is a directory tree of Markdown concept documents per OKF v0.1:
brain/
├── index.md # generated navigation (reserved, progressive disclosure)
├── log.md # generated update history (reserved, newest-first)
├── tables/
│ ├── index.md
│ └── orders.md # a concept: frontmatter + markdown body
└── playbooks/
└── freshness.md
Every non-reserved .md file is a concept. Frontmatter requires only type; title, description, resource, tags, and timestamp are recommended, and unknown keys are preserved. When title is omitted, display names (index entries, MCP resource names, search/list hits) are derived from the filename per spec §4.1 — customer-order-history.md becomes "Customer Order History" — and search/list hits carry titleDerived: true so agents can tell a derived title from an authored one. The bundle-root index.md may declare an okf_version in its frontmatter (spec §11): list_bundles and graph_summary report it, and validate_bundle warns — without failing — when it names a newer major version than the server supports. The same frontmatter may declare a description — one line saying what the bundle is for, written for an agent deciding whether to look inside (e.g. description: "Payments-team runbooks and schema notes"): list_bundles and list_remote_bundles report it, the root index.md MCP resource carries it, and like the rest of the root frontmatter it survives index regeneration. The concept ID is the file path without .md (tables/orders). Relationships are ordinary markdown links — bundle-absolute (/tables/orders.md, recommended) or relative (./customers.md) — and become directed edges in the graph. Broken links are warnings, never errors.
Writes regenerate every index.md as a generated artifact, with two exceptions for human curation (spec §6 supports hand-curated indexes with meaningful section groupings). An index.md whose frontmatter declares generated: false is treated as hand-curated and never rewritten — regenerate_indexes reports it as skipped, and deletes leave its directory in place. And the bundle-root index.md's frontmatter always survives regeneration: a declared okf_version and any extension keys are carried over; okf_version is stamped only when absent.
To view the brain in Obsidian, open the bundle directory as a vault (File → Open folder as vault). The generated index.md files double as navigation pages, and standard markdown links work as-is.
Resources: one text/markdown resource per document, at okf://<bundle>/<path>.
The server also declares MCP server-level instructions — a short primer on OKF conventions (concept IDs, link form, reserved files, the read/write tool flow) that clients inject into the agent's context. Authoring guidance is included only when the server runs with --writable.
The two entry-point tools, search_concepts and get_concept, declare anthropic/alwaysLoad in their _meta, so clients that defer tool loading keep their schemas visible while the rest of the toolset loads on demand.
Read tools:
| Tool | Purpose |
|---|---|
list_bundles |
Configured bundles with concept counts, read-only flags, each bundle's declared description, and a loaded marker for lazily mounted colocated bundles |
get_bundle_guide |
Each colocated root's AGENTS.md guide in full plus every bundle's one-line description; registered only while a colocated root is mounted (dynamically, on the first runtime mount) |
reload_bundles |
Re-read bundles (disk, remote tree, or archive) to pick up external edits; reports added/removed/changed concepts. No-arg form covers loaded bundles; naming an unloaded discovered bundle loads it |
load_remote_bundle |
Index a read-only bundle from a public GitHub tree URL or a .tar.gz/.tgz/.zip archive, in memory only |
load_colocated_remote_bundles |
Mount a published colocated root by URL — every subfolder becomes its own read-only bundle — returning the root AGENTS.md guide inline in the result |
list_remote_bundles |
Remote bundles currently loaded, with their source URLs and declared descriptions |
list_concepts |
Concept metadata (including the resource URI when set), filterable by prefix/type |
get_concept |
One full document: frontmatter, body, outgoing links, and a sections heading list; pass section to fetch a single body section |
get_citations |
Numbered # Citations entries for a concept (spec §8), each classified external / concept / missing; ../ targets resolving into a colocated sibling count as concept. Duplicate # Citations sections are merged, so an accidental empty duplicate cannot mask entries |
read_document |
Raw markdown of any bundle document by path, including reserved index.md / log.md; a missing index.md is synthesized from frontmatter (spec §6, marked synthesized: true) — the entry point for remote bundles published without index files |
search_concepts |
Text query + type/tag/path/link/orphan filters, paginated; an exact-resource filter maps an asset URI to its concept; hits include match locations, a body snippet, and the enclosing section heading |
list_types |
Distinct concept type values with usage counts |
list_tags |
Distinct tag values with usage counts |
suggest_concept_path |
Where a new concept should live, ranked by where same-type (and same-tag) concepts already are |
graph_summary |
Compact overview: counts, types, tags, orphans, derived crossBundleEdges |
get_neighbors |
Bounded expansion around a concept (in/out/both, depth); crossBundle: true follows derived edges into other mounted bundles |
find_path |
Shortest directed link path between two concepts; crossBundle: true accepts bundle:concept IDs and crosses bundles |
export_graph |
Graph as json, dot, or mermaid; crossBundle: true exports all mounted bundles as one namespaced graph with dashed derived edges |
concept_history |
Git commit history for a concept file, newest first, following renames |
concept_diff |
Unified git diff of a concept file against a ref (default: its most recent change) |
validate_bundle |
OKF v0.1 conformance errors + soft warnings (broken links, dangling ../ links into colocated siblings, malformed recommended frontmatter fields, malformed or unresolved citations, duplicate top-level headings, index.md / log.md structure checks); warnings with a safe mechanical fix name their okf-mcp repair fixer id |
concept_history and concept_diff require the bundle to live inside a git work tree; on non-git bundles they return a not a git repository result instead of failing.
Write tools (only with --writable):
| Tool | Purpose |
|---|---|
write_concept |
Create/update a concept (defaulting timestamp to the write time), append a log.md entry, regenerate index.md files. Ordered-list entries under # Citations (1. [text](target)) are normalized to the spec §8 [n] [text](target) form |
update_concept |
Partial update: shallow frontmatter patch (an explicit null deletes a key) and/or replace one body section by heading — everything else, YAML comments and formatting included, survives byte-for-byte. timestamp refreshes to the write time like write_concept (a concept without one gains it in its spec-order slot) unless the patch names it or keepTimestamp: true pins it. Section content may include the heading — a leading repeat of it is stripped, never duplicated — and # Citations entries are normalized like write_concept; log + reindex |
delete_concept |
Delete a concept (optionally refusing while inbound links exist), log it, regenerate indexes |
rename_concept |
Move a concept to a new path, rewriting inbound links across the bundle, log it, regenerate indexes |
promote_concept |
Move a concept into another writable bundle (explicit toPath, or suggest_concept_path-style placement), leaving a citation stub at the old path that points at the promoted copy (relative ../<bundle>/<path> link between colocated siblings, canonical location otherwise) — or stub: false to just report dangling inbound links; logs and reindexes both bundles |
append_log_entry |
Record a change-narrative entry in the bundle-root log.md — or a per-directory one via directory — without touching any concept |
regenerate_indexes |
Rewrite index.md navigation from frontmatter, reporting hand-curated indexes (generated: false) it skipped |
Writes are constrained to safe relative .md paths inside the bundle; reserved filenames (index.md, log.md) and dot-directories are rejected as concept paths.
The automatic log entry from a concept write, update, delete, or rename goes to the nearest existing directory-level log.md above the concept (spec §7 scoped logs), falling back to the bundle root's; the auto path never creates per-directory logs — start one with append_log_entry and subsequent concept changes under that directory keep it current. A rename that crosses scopes is logged in both the old and new paths' logs so neither history has a gap.
okf-mcp --bundle [id=]<path> [--colocated-bundles <root> [--only <a,b,c>]]
[--remote-bundle id=<url>] [--colocated-remote-bundles <url>]
[--canonical-url [id=]<url>] [--writable] [--watch] [command]
mcp Start the stdio MCP server (default)
inspect Print a summary of each bundle's graph
validate Report conformance errors and warnings (exit 1 on errors)
search <query> Search concepts
concept <id> Print one concept document as JSON
graph [format] [bundle]
Export the link graph (json | dot | mermaid | html)
index Regenerate index.md files (requires --writable)
pack [bundle] Publish a bundle as a distributable archive
repair [bundle] Detect and auto-fix known bundle defect classes
(dry-run by default; --write applies)
graph exports the link graph in the named format. With several bundles mounted and no bundle argument, all of them export as one merged graph — node IDs namespaced bundle:concept, with derived cross-bundle edges included and rendered dashed in dot/mermaid — so a --colocated-bundles root exports whole. Name a bundle to scope the export to it (unqualified node IDs, same as the single-bundle output). --include-external adds external link targets as opaque nodes; a URL that derived a cross-bundle edge is not duplicated as one. --out <file> writes the export to a file instead of stdout.
The html format renders the graph as one self-contained interactive page — embedded data plus a small hand-rolled force simulation on <canvas>, no CDN or network access — so the file is shareable and viewable anywhere a browser opens it:
okf-mcp --colocated-bundles /path/to/vault graph html --out graph.htmlNodes are colored by community, with a legend (click an entry to focus that community: everything else fades — edges keep full strength while either endpoint is inside — and clicking the entry again, or the background, clears the focus): a merged multi-bundle export groups by bundle — each bundle reads as a cluster, with cross-bundle edges emphasized between them (bright dashed gold, wider and more opaque than in-bundle links) — while a single bundle groups by concept type unless --community folder (first path segment of the concept ID; top-level concepts group as (root)) or --community tag (first frontmatter tag; (untagged) when absent) overrides. Because bundle grouping always wins for a merged graph, --community is rejected there — name a bundle to use it. Every edge carries a direction arrowhead at its target, colored to match the edge. Node radius scales with degree, hovering shows id/title/description/tags, clicking a node highlights its neighbors, a search box filters by case-insensitive substring against id/title/tags (non-matching nodes fade; an edge stays bright only when both endpoints match), and the view supports node dragging, wheel zoom, and panning; external nodes (--include-external) render as muted squares. Titles and descriptions are embedded with < escaped, so a </script> in a document cannot break out of the page.
pack emits a .tar.gz (or .zip, by --out extension) of a mounted bundle for exchange with systems that can't reach its git remote — the counterpart of --remote-bundle, which loads such archives back. index.md files are regenerated in-memory from the packed concept set so the archive is self-describing, with the bundle root's declared frontmatter (including okf_version) preserved and hand-curated indexes (generated: false) traveling verbatim; the source bundle is never written, so pack needs no --writable and read-only remote bundles can be re-exported too. Repeatable --include/--exclude globs select concepts and logs with the same semantics load_remote_bundle uses (regenerated indexes are always emitted and describe only what was packed):
okf-mcp --bundle brain=/path/to/bundle pack --out brain.tar.gz --exclude 'drafts/**'Relative ../<sibling>/... links into colocated siblings only mean something while the shared layout holds, so the archived copy carries the sibling's canonical concept URL instead (its blob form for GitHub canonicals) — a spec §8 citation form that resolves anywhere; only the link targets change, every other byte travels verbatim, and the source files stay untouched. A resolving link whose sibling has no canonical URL (explicit or derived from a root-level --canonical-url) fails the pack rather than shipping a dead link.
repair runs a registry of named auto-fixers over the mounted bundles (or one named bundle) — a bundle doctor for defect classes discovered in documents already on disk, each pairing detection with a mechanical, provably-safe splice-based rewrite (human formatting and unknown frontmatter survive byte-for-byte outside the touched spans). It dry-runs by default, printing per-fixer, per-file findings as JSON with fixed/skipped counts; --write applies the safe rewrites, appends a log.md entry summarizing the sweep (fixer ids + file counts), and regenerates indexes, matching the authoring tools' bookkeeping. A finding whose rewrite cannot be proven safe is reported instead of guessed at, read-only remote bundles are skipped, and validate warnings with a safe auto-fix name their fixer id. --list prints the registry; for this command --only names fixers, not colocated subfolders:
okf-mcp --bundle ./kb repair # dry-run: report findings
okf-mcp --bundle ./kb repair --write # apply all fixers
okf-mcp --bundle ./kb repair --only citation-format --write
okf-mcp repair --list # show the fixer registryThe initial fixers: citation-format normalizes ordered-list citation entries (1. [text](target), 1) ...) under a # Citations heading to the spec §8 [n] [text](target) form — the same transformation the write paths apply, so repair and write-time prevention cannot drift; duplicate-citation-headings merges duplicate # Citations sections by dropping empty duplicates (the damage pattern where an empty first copy masked every entry from first-match readers), reporting duplicates that each have content for manual merging; okf-uri-to-canonical rewrites okf://<bundle>/<path> citation targets and frontmatter resource URIs — the fallback form promote_concept writes when the target bundle has no canonical URL — to the bundle's canonical URL once one is configured, reporting URIs whose bundle is unmounted or still has none.
--colocated-bundles <root> (repeatable) mounts every immediate subdirectory of a shared root as its own bundle; --only <folder,folder,...> restricts the mount to the named subfolders — see colocated bundles. --colocated-remote-bundles <url> (repeatable) does the same for a published root — a GitHub tree URL or .tar.gz/.tgz/.zip archive — mounting each subfolder as a read-only bundle; see consuming a published colocated root.
--canonical-url id=url (repeatable) declares a bundle's published canonical URL for cross-bundle awareness; with a colocated root's path as the id — or a bare URL when exactly one colocated root is configured — every bundle under the root derives <url>/<folder>, with explicit per-bundle flags taking precedence.
--watch (mcp only) auto-reloads local bundles when .md files change on disk, debounced so an editor save burst triggers one reload; .obsidian/ and other dot directories are ignored. Remote bundles still reload only via the reload_bundles tool. Where recursive fs.watch is unsupported, the server logs a note to stderr and continues without watching.
npm run typecheck # tsc over src + tests
npm test # node:test via tsx
npm run build # emit dist/Source layout: frontmatter.ts / parser.ts (document parsing, link extraction, and body sections), bundle.ts / store.ts (loading and the in-memory index), remote.ts (read-only bundles from public GitHub trees and tar.gz/zip archives), pack.ts (the pack command's archive writer), repair.ts (the repair command's auto-fixer registry), canonical.ts (canonical-URL matching for derived cross-bundle edges), graph.ts / search.ts (traversal, structured search, and vocabulary listings), visualize.ts (the graph html self-contained force-directed export), validate.ts (conformance), git.ts (history/diff via the bundle's git repo), suggest.ts (concept placement suggestions), authoring.ts (the only write path), watch.ts (the --watch file watcher), server.ts (MCP wiring), cli.ts (entry point).
Without --watch there is no file watcher: call reload_bundles after editing bundle files outside the server (e.g. in Obsidian). Concepts written through write_concept refresh the index immediately.