MCP
Tools the OpenKnowledge MCP server exposes to AI agents.
The MCP server gives AI agents structured access to your knowledge base. A few tools answer without a running server: exec (fs-direct shell reads straight off the disk — only the backlink and forward-link enrichment needs the server; history comes from the shadow repo on disk), plus config and palette. preview_url resolves from the project's lock files, but treats opening a preview as demand: if no server is running it auto-starts one (under the OK_MCP_AUTOSTART gate).
Every other read tool (search, links, history, etc.) and all write tools route through the Hocuspocus server, which OpenKnowledge auto-starts on the first call that needs it, under the same OK_MCP_AUTOSTART gate. All tools take a cwd (an absolute path inside the target project) that selects which project the call lands on; a globally registered server requires it unless the client advertises exactly one root.
The surface is 21 tools. The four write verbs — write, edit, delete, move — are native CRUD operations polymorphic over a target. write, edit, and delete nest per-target fields inside the address key (write({ document: { path, content } })) and take exactly one target per call: document, folder, template, or skill — plus asset for write and delete, and a documents batch on write that writes several docs in order, each entry carrying its own summary. move takes flat from/to paths and auto-detects a document, folder, or asset, with nested template and skill targets for those two.
To point an MCP client at this server, locally or over a tunnel from another machine, see Connect remote agents.
Tools
| Tool | Purpose |
|---|---|
exec | Read-only shell (cat, ls, grep, find, head, tail, wc, sort, uniq, cut); reads return frontmatter, backlinks, recent history, and any unresolved comments left on the doc. When the frontmatter schemas plugin is on, doc and folder entries also carry schemas: … naming the JSON Schema files that govern them — the contract to read before writing. Fs-direct; works without the server. |
search | Ranked workspace search (title boost + body BM25 + recency; same engine the cmd-K palette uses). intent: "omnibar" narrows to fast title/path matching (full_text, the default, includes the body); scopes filters hit kinds; limit caps rows (default 20, max 100). When semantic search is enabled, an embeddings signal is additionally fused into full_text ranking (opt-in, content egress) — pass semantic: false to force pure-lexical. Right after boot, an empty result set with ready: false means the index is still building — retry after a couple of seconds rather than treating it as no matches. |
links | Wiki-link graph. kind selects: backlinks, forward, dead, orphans, hubs, or suggest. Accepts an array (e.g. ["dead", "orphans", "hubs"]) to fetch several views in one call; each view nests under its own key in the response. dead is the raw graph view: unlike audit, it still lists unresolved links whose source is a skill document (always) or a lowercase-stemmed log.md/log.mdx (while the project's default-on validation.suppressLogLinkAdvisories is on, with audit reporting brokenLinkSuppression when it withheld them) — read those deliberately, not as a repair queue |
lint | Report content-rule problems (markdownlint style rules, frontmatter schema validation, document-level OKF checks) for one document or a whole-project audit (path scopes it); each finding carries source/code/message/range/severity, audit capped at 10 files × 10 diagnostics and project-wide at 10 warnings with accurate totals; configuration or runtime degradation problems ride a separate warnings channel, with omittedWarningCount when warnings are dropped. Successful responses include ran, the enabled document-lint source families selected for that call (markdownlint, frontmatter, and/or document-level okf). A family absent from ran was not checked, and [] means no checks were selected at all. Pass fix: true with document to auto-fix fixable rules in place — the fix lands through the collaborative document (attributed, live preview), not a shell edit; the rest need edit/write. A fix that landed but could not be re-verified sets diagnosticsArePreFix (branch on it) and explains itself in reLintFailure, whose reason is a closed discriminant and whose message is the prose. Example output: content rules overview |
audit | Unified read-only validation audit: every content problem — content-rule violations (markdownlint, frontmatter schemas, OKF) and broken internal links — in one call, grouped by the file to fix (path scopes to a folder or doc). Each finding is tagged with its validator; broken links report under the source doc that contains them, at the offending line, as warnings by default (the project's validation.links setting can raise them to errors or hide them). Skill documents are excluded from the broken-link half: a skill's links routinely name files it creates only at runtime (use links({ kind: "dead" }) to see them; links from other documents to a skill still validate; documents that merely sit under a dot directory, such as .github/CI_RUNBOOK.md, keep their findings). A lowercase-stemmed log.md or log.mdx at any depth is excluded the same way while the project's default-on validation.suppressLogLinkAdvisories setting is on, because a log's entries deliberately reference pages that moved or were never written, and repairing them would rewrite the history (links({ kind: "dead" }) is unconditional and still reports them, and LOG.md is an ordinary document that keeps its findings). Successful responses include ran: enabled document-lint families plus links unless link validation is off. Document and project-tree OKF checks share the okf family, and a family absent from ran was not checked. A selected family remains in ran if it degrades, with the reason explained in warnings; a partial degradation may still have contributed findings. Same 10 × 10 output cap as lint's audit, plus a warning cap set project-wide at 10 warnings, with accurate totals and omittedWarningCount when warnings are dropped. No fix shape — lint fixes go through lint, link repairs through edit/write |
history | Version timeline for a document, folder, or skill. Each entry carries a version (commit SHA) you pass to restore_version. Filter with kind (checkpoint / wip / upstream), author / excludeAuthor, and branch; paginate with limit / offset (default 50, max 200). folder: "" selects the project-root timeline |
skills | Find skills. Pass query to search skills.sh and return import-ready rows (name, source, installs, publisher); omit name to list every managed skill across Project + Global (rows carry installed + hosts); pass name to read one (addressed by name+scope, never by path); pass name + file to read one bundle file's text — the read path for references/ and scripts/ content |
config | Read the effective merged config (config({ key: "appearance.theme" }) for a sub-tree; omit key for the whole config) |
palette | Markdown-native authoring forms, themed html preview embed starters, and theme tokens. Pass components for the canonical components' full JSX prop schemas |
preview_url | Resolve the browser-reachable preview URL. Targets a document, folder, or skill in the current project, or — via file, an absolute path — a markdown file outside it (resolved through the session serving that file). Per-response previewUrl fields elsewhere are route-only (/#/<doc>); call this when you need the full openable URL |
share_link | Build a GitHub-substrate share URL for a doc or folder to send to a teammate (kind disambiguates; path: "" with kind: "folder" shares the content root). Responses carry a freshness verdict — current, stale (unpushed changes), or absent (never pushed), omitted when the probe can't run — and prepend a matching warning to relay with the link. Read-only against .git/; never publishes |
write | Create or overwrite a document, folder, template, skill, or asset. For a document: { path, content }, or { path, template } to instantiate from a folder template. position selects replace (full rewrite; the only mode that touches frontmatter), append, or prepend. A replace is refused if another writer changed the document in the last few seconds: wait and retry, or use append, prepend, or edit, which are never refused. extension selects .md (default) or .mdx for a new document; an existing doc keeps its extension (the MCP cannot change it). An asset takes exactly one of content (base64, for small files) or source (a local path the server reads, for large files) |
edit | Modify a document (body find/replace or a frontmatter merge-patch), a folder (frontmatter merge-patch), a template, or a skill. Body edits take { path, find, replace, occurrence? }; occurrence selects which match (1 = first) |
delete | Delete a document (one path or an array), folder, template, skill, or asset |
move | Move or rename a document, folder, asset, template, or skill, rewriting every affected link (templates and skills carry no inbound links). A skill also moves between Project and Global scope via scope + toScope: history does not transfer; the editor locations the destination level can host are re-projected there, while everything else (the agents hub, custom roots, and any editor with no skills root at that level) is removed at the source and not re-created at the destination. The removed locations come back as droppedLocations; re-add the ones that level can place with install. That only holds on a success: on a failed move the same field lists source-side placements the call had already removed, so installing them at the destination would spread a retained copy instead of restoring anything |
install | Manage where a skill is available. A skill is one real folder (its source, which itself loads) plus managed copies/symlinks at other locations — there is no draft state and no "uninstall everywhere". add / remove change the other locations (editor ids, agents, or custom root paths); mode flips an editor copy↔symlink; source relocates the real folder. To make a skill stop existing, use delete |
import | Acquire a skill from a full skills.sh skill-page URL (https://www.skills.sh/<owner>/<repo>/<skill>, or https://www.skills.sh/site/<hostname>/<skill> for a website catalog), owner/repo[/subpath], a git URL, or a local / file:// path into the vendor-neutral .agents/skills/<name>/ hub as versioned content — live immediately, with cross-harness projection via install. Pass the required add array with at least one install location (an editor id, agents, or a custom root path). Use skill to choose one skill from a multi-skill source or a skills({ query }) result. Records provenance in .ok/skills-lock.json; a name collision lands under <name>-imported; scripts are imported as content and never executed |
checkpoint | Save a project-wide version snapshot — a single restore point. Returns its version |
restore_version | Restore one document or skill to a historical version (the SHA from history or checkpoint) |
conflicts | Read tracked conflicts. kind: "list" enumerates them ({ file, detectedAt, conflict, reason?, conflictKind?, docName }); kind: "content" returns one file's stages and metadata ({ file, base, ours, theirs, shape, conflict, reason?, conflictKind?, resolutionOptions }). conflictKind: "stale-external-write" identifies a protected stale save; Git working-tree overlays use conflict.kind: "working-tree" |
resolve_conflict | Write a chosen resolution (mine / theirs / content / delete, narrowed by that conflict's resolutionOptions). Only merge-native creates a resolution commit |
Output shape mirrors the input
The write verbs nest their result under the same target key you wrote, mirroring the input: write({ folder }) returns { folder: { ok, path } }, edit({ document }) returns { document: { … } }, and a batch returns { documents: [ … ] }. The preview envelope (previewUrl, previewUrlSource, warning) stays at the top level on write and edit responses; read tools (search, links, exec) attach a route-only previewUrl to each hit or entry instead. conflicts and links nest the same way under their kind.
Advisory warnings
write and edit results may carry a warnings array on the document entry (per entry for a batch); restore_version carries it at the top level. Entries are discriminated by kind. content-divergence and disk-edit-reconciled are write-integrity signals — the stored doc differs from what the call composed, so re-read it before editing further. mermaid-parse-error means the write landed but that fence won't render — fix the fence and re-edit.
delete, move and install on a skill target carry a separate warnings array of plain strings, not kind-discriminated entries. It reports work that did not undo the operation: bookkeeping records the server could not update after the skill itself was already deleted, renamed or moved, and — on a delete — that the directory removed was one an earlier failed move had retained for recovery. The operation succeeded; the warning names what to check or repair.
Instantiating from a template
write accepts a template field on the document target — write({ document: { path, template } }) — that resolves against the cascade of templates available for the target document's parent folder, using the same folder-up walk the Folders and templates page describes. The template body becomes the new document's content; the template's own info block (its name and description) is stripped, so it doesn't bleed onto the document.
content and template are mutually exclusive. With template, position is forced to replace (initial create semantics).
Editing frontmatter
Use edit({ document: { path, frontmatter } }) for frontmatter edits: it applies a top-level merge patch (set or create a key with a value, delete a key by passing null; a nested object replaces that key's whole subtree, and null inside a subtree is rejected). A document body find/replace (edit({ document: { path, find, replace } })) is body-only and refuses patterns that touch frontmatter; write with position: "replace" is the only write mode that includes the ---\n…\n--- block (other positions ignore any frontmatter in the payload). Folders and templates take frontmatter the same way: edit({ folder: { path, frontmatter } }).
Edit summaries
The write tools (write, edit, move, install, checkpoint, restore_version) accept an optional summary describing the intent of the edit, e.g. "Fixed token-refresh race". Summaries appear on the document's timeline so readers can scan recent agent activity.
Cap is 80 characters. Avoid secrets and PII; summaries persist to git history.
Conflict-aware writes
A doc can land in conflict through a Git merge, a pull that collides with a local overlay, or OpenKnowledge's own reconciliation of the editor against disk. Reconciliation covers stale-external-write, which protects an acknowledged edit when another app restores the exact older file version, even with GitHub sync disabled. Every mutating tool (write, edit, delete, move, restore_version, and agent undo) refuses a conflicted doc with an RFC 9457 problem response (409) that names the kind and the strategies that apply to it:
{
"type": "urn:ok:error:doc-in-conflict",
"title": "Document is in conflict.",
"status": 409,
"detail": "The document is in a conflict state. Call conflicts({ kind: \"content\" }) + resolve_conflict before retrying.",
"file": "notes/sso.md",
"conflict": { "kind": "reconcile", "reason": "disk-markers" },
"resolutionOptions": ["mine", "content", "delete"]
}A write that first detects the stale file during its final disk flush returns a different 409, urn:ok:error:stale-external-write. The edit reached collaborative state and the recovery snapshot, but the disk write did not happen. Resolve and re-read before retrying, so you don't duplicate an append that already reached ours.
conflict.kind says where the conflict came from, and the kind decides what a resolve does. merge-native is a git merge that left unmerged stages in the index. Resolving one stages your choice with git and commits the merge once no merge-native conflict is left. working-tree is a pull-only overlay collision, pinned to the origin blob it hit. reconcile is OpenKnowledge's own three-way merge of the editor against disk, including protected stale saves; no git object holds those stages, so OpenKnowledge snapshots them when it detects the conflict. Resolving a working-tree or reconcile conflict commits nothing.
A reconcile conflict also carries a reason: merged-with-markers, refused-conflict-markers, refused-no-base, refused-too-large, disk-markers, or stale-external-write. On that last reason the compatibility field conflictKind is stale-external-write. Every other row uses conflictKind: "git".
A write to a document whose file has been deleted returns urn:ok:error:doc-removed (409): the edit reached collaborative state, but the disk write was refused rather than recreating the removed file. This is not retryable — the document is gone. Re-create it with write if you still want the content.
A write whose document path cannot be resolved inside the content directory returns urn:ok:error:path-escape (400): the edit reached collaborative state, but the disk write was refused. This covers a symlink cycle at the document's own path and a link resolving outside the content directory. It is not retryable — repair the path on disk before writing this content again.
A write refused because the document's duplication baseline could not be read at the final disk flush returns urn:ok:error:store-refused (503): the edit reached collaborative state, but the disk write was refused rather than risk persisting duplicated content. The document file was not updated; the server preserves the edited document, with a copy in its recovery buffer unless that buffer is unavailable or unwritable. Retry only after the filesystem fault is repaired.
A whole-document replace attempted while another writer changed the same document in the last few seconds returns urn:ok:error:concurrent-overwrite-refused (409): nothing was written, so there is no partial edit to undo. This is not tracked conflict state, so conflicts() and resolve_conflict do not apply to it. A single-document HTTP response carries retryAfterSeconds alongside a matching Retry-After header, and the write tool appends the same bound to its error text (Retry after 3s.). POST /api/agent-write-batch keeps its 200 success envelope; each refused entry carries the problem type and retryAfterSeconds, while sibling entries are unaffected. Wait that long and repeat the write, or use position: "append" or "prepend", or edit, which are never refused. Peer writers are distinguished by identity. Anyone supplying an agentId or x-ok-connection-id must use a stable value unique to one writer, containing only ASCII [A-Za-z0-9_-] characters and 1–64 characters long. A nonconforming agentId is discarded: POST /api/agent-write-md and POST /api/agent-write-batch then treat the caller as unidentified, while POST /api/lint/fix falls back to the project principal when available. A nonconforming x-ok-connection-id is silently replaced with a minted identity. A POST /api/agent-write-md caller sends its agentId on every write. The MCP write tools declare no agentId field. OpenKnowledge mints an identity per MCP connection unless an MCP Streamable HTTP client forwards a valid x-ok-connection-id while creating the protocol session. Writers that share or reuse an identity share writer attribution, the server-side write session, and undo history, and are not refused against each other; the server does not detect identity collisions. Having an identity records you as a peer, so a replace carrying a different identity is refused for the next few seconds, while your own recent write does not arm the refusal against your follow-up replace. A writer that has no identity of its own is never recorded as a peer, so it never arms the refusal against a later writer. That covers a POST /api/agent-write-md that omits agentId, and a lint fix that both omits agentId and runs on a project with no principal. A caller that sent no agentId is still refused its own replace when a writer that did send one changed the document in the last few seconds, because that caller cannot be shown to be the recorded writer. Two POST /api/agent-write-md callers that both omit agentId are never refused against each other, though a recent change from a connected editor still refuses both of them.
Resolving a conflict
conflicts({ kind: "list" })returns every tracked conflict as{ file, detectedAt, conflict, reason?, conflictKind?, docName }. Call it to detect conflicts without waiting for a 409.conflicts({ kind: "content", file })returns{ content: { file, base, ours, theirs, shape, conflict, reason?, conflictKind?, resolutionOptions } }.oursis the live Y.Text when the doc is loaded and marker-free; otherwise it falls back to the stage pinned at detection. For a stale external write,oursis the protected content andtheirsis the rejected older save.resolve_conflict({ file, strategy, content? })writes the chosen bytes. Send a strategy from that conflict'sresolutionOptions, not the full set. Areconcileconflict whosetheirsside is raw marker text does not offertheirs, and sending it anyway returns a 422 withrefusal: "strategy-not-offered". The same 422 shape carriesrefusal: "markers-in-content"when the bytes you sent still contain markers, and that field is what tells the two apart. A 404 (urn:ok:error:no-conflict-tracked) means nothing is tracked for that path. Re-read the list; it does not mean your resolution was saved.
What mine and theirs mean depends on the kind. On merge-native they are git checkout --ours and git checkout --theirs (the committed stages 2 and 3), and delete is git rm. On working-tree, mine keeps your overlay verbatim and theirs restores the pinned origin blob. On reconcile, mine uses the latest marker-free live Y.Text, falling back to the captured editor stage when the document is unloaded or contains markers; theirs uses the disk bytes captured at detection. content always writes the bytes you supply, and delete always removes the file.
file is a .md or .mdx path relative to the project directory, extension included. It mirrors the on-disk shape, not the extension-less document path the other tools take.
For how a conflict appears and resolves in the app, see Resolving a conflict. The conflicts and resolve_conflict tool descriptions repeat the per-kind rules at the call site.
Preview links
Read and write tools return a previewUrl for any doc they touched, but those are route-only (/#/<doc> with no host:port), meant to identify the doc, not to be opened as-is. Hosts that want to open the editor (Claude Code Desktop's preview_start({url}), terminal open <url>) call preview_url to resolve the full URL once per session.
preview_url always carries an autoOpen boolean (top-level on its response) reflecting the user's appearance.preview.autoOpen preference. The write tools (write, edit) carry the same boolean on the warning object, which fires only when no browser is attached to the preview. In the common case (a browser is attached), the write response has no warning and no autoOpen field. Agents honor autoOpen before navigating: true (default) follows the host-capability routing in the bundled skill; false means the user is managing their own preview window, so the agent surfaces the URL on request but does not open or refresh anything. The value is resolved fresh on every tool call, so a mid-session toggle propagates within 0–1 calls, with no client restart.