Kobsidian
About
Filesystem-first MCP server for Obsidian vaults with an LLM-Wiki layer on top.
Details
- Author
- bezata
- Downloads
- 440
- Categories
- Knowledge Base, Productivity, Other, File Management
Jump to
- Filesystem-first — works directly on the vault without Obsidian running
- 90 typed MCP tools with Zod validation and client‑safety hints
- LLM‑Wiki orchestration: ingest, index, lint, and cross‑reference pages
- Both stdio and Streamable HTTP transports with optional bearer auth
- Ships via npm, .mcpb bundles, Smithery, and the MCP Registry
Setting up with Highlight
This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:
- Download and install Highlight from highlightai.com/download
- Navigate to the plugins tab and select "Add Custom Plugin"
-
Configure the plugin with the settings below
Plugin Name
KobsidianCommand (node, npx, python, etc.)Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.
- Enable "Start Automatically" if you want the plugin to start when Highlight launches
From the repository
Install via npx -y kobsidian-mcp (or bunx for faster cold start), by dragging a .mcpb bundle into Claude Desktop, through Smithery, or from source. Configure three environment variables: OBSIDIAN_VAULT_PATH, OBSIDIAN_API_URL, and OBSIDIAN_REST_API_KEY (the latter only required for REST-bridged tools). A typical session starts with prompts like “Set up a wiki in this vault,” “Ingest this URL,” or “Audit the wiki,” using the wiki.* tool namespace.
vault.list
List every Obsidian vault kObsidian knows about, merged and deduplicated across three sources: the operator's OBSIDIAN_VAULT_PATH (the default — always included), any OBSIDIAN_VAULT_<NAME>=path env vars (explicit named vaults), and — when KOBSIDIAN_VAULT_DISCOVERY is `on` (the default) — the user's local Obsidian application registry at obsidian.json. Each item reports its `source`, `isDefault`, `isActive`, and `exists` so the LLM can flag stale or missing vaults. Pass `refresh: true` to force a fresh scan instead of using the 30s cache. Read-only. NOTE: the `obsidian-app` source is EXPERIMENTAL — it parses Obsidian's undocumented obsidian.json registry (stable since 1.0 but internal to Obsidian) and may silently stop returning results if Obsidian changes the format; the env-var sources are the documented, stable path. Examples: Example 1 — List vaults using the 30-second cache: ```json {} ``` Example 2 — Force a rescan (obsidian.json changed, new env vars added): ```json { "refresh": true } ```
vault.current
Return the vault that filesystem tools (notes.*, tags.*, dataview.*, blocks.*, canvas.*, kanban.*, marp.*, templates.*, tasks.*, links.*, wiki.*, stats.vault) would resolve to right now, plus the full precedence chain so the LLM can explain to the user why that vault was picked. `reason` is `session-selected` (vault.select was called), `env-default` (fell back to OBSIDIAN_VAULT_PATH), or `none` (nothing configured — tools will fail until vault.select or an env var is set). When OBSIDIAN_API_URL is configured, the response also carries an `obsidianLiveInstance` note reminding the caller that workspace.* and commands.* tools target whichever vault the live Obsidian process has open, NOT the filesystem vault selected here. Read-only.
vault.select
Set the session-active vault for subsequent filesystem tool calls. Identify the target by EXACTLY ONE of `id` (stable id from vault.list), `name` (case-insensitive match), or `path` (absolute directory path — need not appear in vault.list; lets the LLM point at a fresh/empty vault to initialise). Precedence chain becomes: per-call `vaultPath` argument (highest) → this session selection → OBSIDIAN_VAULT_PATH → error. Explicit `vaultPath` arguments on individual tool calls always override this selection. Respects KOBSIDIAN_VAULT_ALLOW / KOBSIDIAN_VAULT_DENY operator gating (though OBSIDIAN_VAULT_PATH is never filtered). Does NOT change which vault the live Obsidian process has open — `workspace.*` and `commands.*` tools remain tied to OBSIDIAN_API_URL. HTTP deployments: this server shares the selection across HTTP clients, so concurrent multi-client HTTP setups should pass `vaultPath` per call instead. Examples: Example 1 — Switch to the vault named 'Work': ```json { "name": "Work" } ``` Example 2 — Select by id from vault.list: ```json { "id": "58f115bd2c2febd2" } ``` Example 3 — Point at an ad-hoc path (e.g. a fresh vault to initialise): ```json { "path": "/Users/alice/FreshVault" } ```
vault.reset
Clear the session-selected vault so the precedence chain falls back to OBSIDIAN_VAULT_PATH. Use this to signal 'I'm done with the scratch vault, go back to the default'. Idempotent — running on an already-cleared session is a no-op that reports `changed: false`. Does not change per-call `vaultPath` behaviour.
notes.read
Read a note and return any combination of its body, parsed frontmatter metadata, and lightweight statistics. `include` selects which sections to return — default is `['content', 'metadata']`. Ask for `['stats']` alone when you only need word/character/heading/link/task counts and want to skip loading the full body. Read-only. Fails with `not_found` when `path` does not exist. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
notes.create
Create a new note or folder in the vault. `kind:'note'` creates a markdown note at `path` with the given `content`; `ifExists` controls collision behavior (`error` = fail, default; `replace` = overwrite; `skip` = no-op). `kind:'folder'` creates a directory at `path` (intermediate folders are created automatically; idempotent — re-creating an existing folder is a no-op). Returns the standard mutation envelope. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Create a new note, failing if it exists: ```json { "kind": "note", "path": "Journal/2026-04-24.md", "content": "# Today\n" } ``` Example 2 — Ensure a folder exists (idempotent): ```json { "kind": "folder", "path": "Projects/Alpha/Reports" } ```
notes.edit
Mutate the body of an existing note. The `mode` field selects how `content` is applied: `replace` overwrites the whole note; `append` adds to the end; `prepend` adds after the frontmatter (or at the top if none); `after-heading` inserts after the first heading whose text matches `anchor` (no leading `#`); `after-block` inserts after the block reference `^anchor`. Fails if the note does not exist — use `notes.create` first. `replace` mode is idempotent-destructive; the others are additive. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Append a new journal entry to the end of today's note: ```json { "mode": "append", "path": "Journal/2026-04-24.md", "content": "\n## Afternoon\n\nFinished the tool consolidation." } ``` Example 2 — Insert content after a specific heading: ```json { "mode": "after-heading", "path": "Projects/Alpha.md", "anchor": "Open questions", "content": "- Do we need to bump the Zod major?\n" } ``` Example 3 — Insert after a block reference: ```json { "mode": "after-block", "path": "Notes/idea.md", "anchor": "idea-1", "content": "Follow-up thought …" } ```
notes.frontmatter
Set or unset fields in a note's YAML frontmatter. `set` is a map of `{field: value}` pairs to write; `unset` is a list of field names to delete. `strategy:'merge'` (default) leaves unspecified fields untouched; `strategy:'replace'` overwrites the entire frontmatter block with `set` (any field not in `set` is dropped). At least one of `set` or `unset` is required. Idempotent — re-running with the same arguments converges on the same frontmatter state. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Set two fields, merging with existing frontmatter: ```json { "path": "Projects/Alpha.md", "set": { "status": "in-progress", "owner": "behzat" } } ``` Example 2 — Remove a field: ```json { "path": "Projects/Alpha.md", "unset": [ "draft" ] } ```
notes.delete
Delete a note from the vault. Destructive — the file is removed from disk. Fails with `not_found` when the path does not exist. There is no undo; use with care. For folders, call `notes.move` to an archive location instead (folder deletion is not exposed as a tool to avoid accidental cascading deletes). Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
notes.move
Move a note or folder to a new path. `kind:'note'` moves a single `.md` file; `kind:'folder'` moves a directory and every note beneath it. When `updateLinks:true` (the default), wiki and markdown links elsewhere in the vault that reference the moved path are rewritten to point at the new location. Destructive — overwrites or replaces existing content at the destination. Fails when the source does not exist. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
notes.list
List notes and/or folders in the vault, optionally scoped to a `folder` and filtered by creation/modification date. `include` selects what to return (`notes`, `folders`, or `both`). `recursive:true` descends into subfolders. `since`/`until` (ISO dates) combined with `dateField` (`created` or `modified`, default `modified`) narrow the result by date. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
notes.search
Full-text search across every note in the vault. The `query` supports plain text and lightweight prefix filters: `tag:foo` restricts to notes carrying `#foo`, and `path:Journal/` restricts to notes under a folder. `contextLength` controls how many characters of surrounding context are returned per match (default 80). Read-only. For pure tag or date filtering, `tags.search` and `notes.list` are faster. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tags.modify
Mutate the frontmatter `tags` list of a single note. Four ops are supported: `add` unions the incoming tags with the existing list (duplicates dropped); `remove` drops any incoming tag currently present; `replace` overwrites the list entirely; `merge` is an alias for `add`. Leading `#` on incoming tags is stripped automatically. This tool only touches the frontmatter block — inline `#tag` occurrences in the body are left untouched. Idempotent: repeated calls with the same op and tags converge on the same result. Returns `{changed, target, summary, op, tagsAfter}`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Add two tags to a note (idempotent): ```json { "path": "Projects/Alpha.md", "op": "add", "tags": [ "in-progress", "priority/high" ] } ``` Example 2 — Replace a note's entire tag set: ```json { "path": "Inbox/today.md", "op": "replace", "tags": [ "processed" ] } ```
tags.search
Find every note in the vault that contains a given tag, either in frontmatter `tags` or as an inline `#tag` in the body. Leading `#` on the query is stripped. For each hit, the result carries `{file, absolutePath, tagLocations: {frontmatter, inline}}` so callers can distinguish where the tag came from. Read-only. For analyzing tags of ONE specific note (not a vault-wide search), use `tags.analyze` instead. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tags.analyze
Return the tags present in a single note, split into `frontmatterTags`, `inlineTags`, and their de-duplicated union `allTags`. Use this when you have one note and want to know what tags it carries — contrast with `tags.search`, which scans the whole vault for one specific tag. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tags.list
List every unique tag used across the vault (frontmatter and inline combined). With `includeCounts: true`, each item includes how many notes carry the tag; `sortBy` lets you sort by `name` or `count` (the latter requires counts). Read-only. For finding notes carrying a specific tag, use `tags.search`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.backlinks
Find every note that links TO a target note (inbound references). Supports both wiki-style `[[Note]]` and markdown-style `[text](Note.md)` links. When `includeContext:true`, each result carries a `contextLength`-char snippet of surrounding text so the agent can judge link intent without re-reading each source. Read-only. For outbound links (what a note points AT), use `links.outgoing`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.outgoing
Extract every link FROM a note (outbound references) — wiki-style `[[…]]` and markdown-style `[…](…)`. When `checkValidity:true`, each entry carries a `valid` flag indicating whether the target path resolves in the vault. Read-only. For inbound references (what points AT the note), use `links.backlinks`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.broken
Find every link in the vault (or a `directory` subtree) whose target does not resolve to an existing note. Each result carries the source file, line number, link text, and unresolved target. Read-only. Pair with `notes.move` (with `updateLinks:true`) to fix them after moves. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.graph
Build a full vault link graph: every note becomes a node, every outbound link becomes a directed edge. Return shape is `{nodes, edges, stats}` where nodes carry basic metadata (path, title) and edges carry source/target and link kind. Expensive for large vaults — prefer `links.backlinks`, `links.outgoing`, or `links.connections` for targeted queries. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.orphaned
Return every note with zero incoming AND zero outgoing links — i.e., notes that are disconnected from the rest of the vault graph. Useful for cleanup passes. Read-only. Often paired with `links.hubs` and `links.broken` in a weekly vault-health routine. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.hubs
Return notes with at least `minOutlinks` outgoing links (default 10), sorted by outbound count descending — the vault's connective tissue / MOCs / curated indexes. Each result carries `{path, title, outbound, inbound}`. Read-only. Use this to find pages that already act as navigational anchors (good seeds for `links.connections`); use `links.health` for a single rolled-up score across the whole vault, and `links.graph` when you need the full raw edge list rather than just the dense nodes. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.health
Summarise link health for the whole vault: total link count, broken-link count and ratio, orphan-note count, average outbound/inbound link density, and a list of the top hub notes. Read-only. Use this as a dashboard check; call `links.broken`/`links.orphaned`/`links.hubs` for the full per-item lists. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
links.connections
Explore the graph neighbourhood around a seed note — direct and multi-hop connections up to `depth` hops (default 2). Returns the set of reachable notes plus the paths that reach them. Higher `depth` values blow up result size quickly; keep it ≤3 unless you know the graph is sparse. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
stats.vault
Return aggregate statistics for the whole vault: total note count, total word count, total character count, total task count (open and completed), tag usage summary, and file size footprint. Read-only, scans every `.md` file. For per-note statistics use `notes.read` with `include: ['stats']`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tasks.search
Scan the vault for Tasks-plugin-style markdown task lines (`- [ ]` / `- [x]`) and filter by status, priority, due date range, recurrence, or tag. Result items include the task text, source file, line number, status, and parsed metadata — enough to locate and further manipulate each task via `tasks.toggle` or `tasks.updateMetadata`. `sortBy` controls ordering; `limit` caps the result count. Read-only. For vault-wide counts without per-task detail, use `tasks.stats`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tasks.create
Append a new task line to a note. The task is written in Tasks-plugin format: `- [ ] <content> {metadata emojis}`. Optional metadata (`priority`, `dueDate`, `scheduledDate`, `startDate`, `doneDate`, `createdDate`, `recurrence`) is encoded as the plugin's convention emojis (🔺⏫📅⏳🛫✅➕🔁). Returns the standard mutation envelope with the 1-based `lineNumber` where the task was inserted. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Append a simple task with a due date: ```json { "filePath": "Tasks.md", "content": "Write the v0.3.0 migration doc", "dueDate": "2026-05-01" } ``` Example 2 — Append a high-priority weekly recurring task: ```json { "filePath": "Tasks.md", "content": "Weekly review", "priority": "high", "recurrence": "every week on Sunday" } ```
tasks.toggle
Flip a task line between `[ ]` and `[x]` in place, identified by `sourceFile` and 1-based `lineNumber`. When marking a task done, a `✅ YYYY-MM-DD` date is stamped into the line (default today; override with `doneDate`). Fails if the target line is not a task checkbox. Use `tasks.search` to find the right `sourceFile`/`lineNumber` pair. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tasks.updateMetadata
Update a task's dates, priority, or recurrence expression in place without touching the task body text. Identified by `sourceFile` + 1-based `lineNumber`. Pass only the fields you want to change. Idempotent — re-running with identical inputs converges on the same line. Fails if the target line is not a task. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
tasks.stats
Return aggregate task statistics for the whole vault: total tasks, incomplete count, completed count, overdue count (due date passed and still incomplete), upcoming counts by horizon (today/this-week/next-week), and per-priority breakdown. Read-only. Use `tasks.search` to get the individual task records. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.query
Execute an arbitrary Dataview Query Language (DQL) query through the Obsidian Local REST API. The query string is raw DQL — e.g. `LIST FROM #inbox`, `TASK WHERE !completed`, `TABLE file.mtime FROM "Journal"`. Requires the Dataview plugin to be enabled in Obsidian and the Local REST API plugin to be configured (OBSIDIAN_API_URL/OBSIDIAN_REST_API_KEY). For common patterns (list-by-tag, list-by-folder, table) the sugar tools `dataview.listByTag`/`listByFolder`/`table` are easier to use — prefer those when applicable and fall back to `dataview.query` for custom DQL. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.listByTag
Convenience wrapper that runs `LIST FROM #tag` (optionally with `WHERE`, `SORT`, and `LIMIT` clauses). Returns the same shape as `dataview.query`. Requires the Dataview and Local REST API plugins. Use this instead of authoring raw DQL when filtering by a single tag. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.listByFolder
Convenience wrapper that runs `LIST FROM "folder"` (optionally with `WHERE`, `SORT`, and `LIMIT` clauses). Useful when you want every note under a vault folder. Requires the Dataview and Local REST API plugins. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.table
Convenience wrapper that runs `TABLE field1, field2, … FROM …` with optional `WHERE`, `SORT`, and `LIMIT` clauses. Use this when you need structured columnar output. Requires the Dataview and Local REST API plugins. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.index
Parse a single note and return everything Dataview would index from it: page-level metadata (title, aliases, tags, frontmatter fields), list-item fields, task-line fields, and both DQL and DataviewJS block locations. Read-only, runs locally (does NOT require the Local REST API). Use this to understand what Dataview sees in a note without running a query. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.fields.read
Read Dataview fields from the vault. `op:'extract'` returns every field declared in a single note (page, list-item, and task-line fields combined). `op:'search'` scans the whole vault for notes whose fields match a `key` (and optionally a `value` coerced by `valueType`); use `scope` to restrict which field kinds are considered. Read-only. For mutating fields, use `dataview.fields.write`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
dataview.fields.write
Insert or remove a Dataview field in a single note. `op:'add'` inserts a `key:: value` field; `syntaxType` picks the rendering (`full-line` = own line; `bracket` = `[key:: value]`; `paren` = `(key:: value)`); `insertAt` chooses placement (`start`, `end`, `afterFrontmatter`) unless `lineNumber` is given for precise control. `op:'remove'` deletes every occurrence of `key` (optionally restricted to a single `lineNumber` or a Dataview `scope`). Idempotent — re-running with the same args converges on the same document state. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Add a full-line priority field after the frontmatter: ```json { "op": "add", "filePath": "Projects/Alpha.md", "key": "priority", "value": "high", "syntaxType": "full-line", "insertAt": "afterFrontmatter" } ``` Example 2 — Remove every occurrence of the `status` field from a note: ```json { "op": "remove", "filePath": "Projects/Alpha.md", "key": "status", "scope": "all" } ```
blocks.list
List fenced code blocks of the supported knowledge-base languages (`dataview`, `dataviewjs`, `mermaid`) in a single note or across the vault. Use this to discover what DQL, DataviewJS, or Mermaid blocks exist before reading or updating them. Omit `language` to list blocks of all three types in one call. Vault-wide scanning is only supported for Mermaid; for Dataview languages a `filePath` is required. Returns `{total, items}` where each item carries at minimum `{filePath, language, index, id?}`. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
blocks.read
Read one fenced block's source and language-specific metadata. Locate the block by `blockId` (preferred, stable) or `index` (0-based within the language group in the file; defaults to 0). `language` is required so the tool can dispatch to the correct parser and return the right metadata (Mermaid directives, Dataview DQL parts, etc.). Fails with `not_found` when no block matches the locator. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
blocks.update
Replace one fenced block's body source-preservingly — the surrounding fences, language tag, and neighbouring content are untouched. Locate the block by `blockId` or `index`. `language` acts as a guard: if the located block is not of the declared language, the update fails. `source` is the replacement body WITHOUT the surrounding ``` fences. Idempotent — re-running with identical inputs is a no-op on the file contents. Destructive — overwrites the previous block body in place. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Replace the first Mermaid diagram in a note: ```json { "filePath": "Diagrams/system-overview.md", "language": "mermaid", "index": 0, "source": "flowchart TD\n A --> B" } ``` Example 2 — Update a DQL query block by stable id: ```json { "filePath": "Dashboards/Inbox.md", "language": "dataview", "blockId": "inbox-open", "source": "TASK\nFROM #inbox\nWHERE !completed" } ```
marp.read
Read some or all of a Marp presentation deck (a markdown file with `marp: true` frontmatter and `---` slide separators). The `part` field selects what to return: `deck` returns the whole deck (frontmatter, all slides, directives); `slides` returns a list of slide summaries (separator and directive metadata, no body); `slide` returns one slide's full source, located by `slideId` or 0-based `index`. Output shape varies by `part` — see the description of each variant. Read-only. Use `marp.update` to mutate. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
marp.update
Mutate a Marp deck in place. `part:'slide'` replaces one slide's body (located by `slideId` or `index`) without touching neighbouring slides. `part:'frontmatter'` merges `fields` into the deck's frontmatter — unspecified fields are preserved; pass `null` to a field to unset it. Idempotent — re-running with identical inputs is a no-op on the file contents. Destructive — overwrites in place. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Replace the second slide's body: ```json { "part": "slide", "filePath": "Decks/launch.md", "index": 1, "source": "# New headline\n\nUpdated body" } ``` Example 2 — Change the deck's theme and set a new title: ```json { "part": "frontmatter", "filePath": "Decks/launch.md", "fields": { "theme": "gaia", "title": "Launch plan" } } ```
kanban.parse
Parse a markdown Kanban board file into its column/card structure. Use this when you need the full board content — each column's name and its cards with their completion state. Works with the obsidian-kanban plugin's markdown format. Read-only. For completion counts and ratios instead of the full card list, use `kanban.stats`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
kanban.stats
Summarise a Kanban board: total cards, completed count, incomplete count, completion rate, and per-column breakdown. Use this for dashboards or progress checks where you don't need each card's full text. Read-only. Use `kanban.parse` when you need the actual card content. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
kanban.card
Add, move, or toggle a card on a Kanban board. The `op` field selects the mutation and determines which other fields are required: `add` needs `columnName` and `cardText` (plus optional `status`, `dueDate`, `position`); `move` needs `cardText`, `fromColumn`, `toColumn` (plus optional `position`); `toggle` needs `cardText` (plus optional `columnName` to scope the search). Missing destination columns are created automatically. Returns a `{changed, target, summary, ...}` mutation envelope. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Add a new card to the Todo column with a due date: ```json { "op": "add", "filePath": "Boards/Project.md", "columnName": "Todo", "cardText": "Write migration doc", "dueDate": "2026-05-01", "position": "end" } ``` Example 2 — Move a card from In Progress to Done: ```json { "op": "move", "filePath": "Boards/Project.md", "cardText": "Write migration doc", "fromColumn": "In Progress", "toColumn": "Done" } ``` Example 3 — Toggle a card's completion in any column: ```json { "op": "toggle", "filePath": "Boards/Project.md", "cardText": "Write migration doc" } ```
canvas.create
Create a new empty Obsidian canvas (`.canvas`) file at the given path. Fails if the path already exists unless `overwrite: true` is passed. Canvas files are JSON documents that Obsidian renders as an infinite spatial whiteboard of nodes and edges. Use `canvas.edit` to add nodes/edges once the file exists. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
canvas.parse
Parse an Obsidian canvas file and return its full structure: every node (text, file, link, group) and every edge. Use this when you need the complete graph; for just the neighbours of a specific node, call `canvas.connections` instead. Read-only. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
canvas.connections
Return the incoming and outgoing edges of a single canvas node. Use this to walk the canvas graph one node at a time without loading the full document. Read-only. For full-graph parsing, use `canvas.parse`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
canvas.edit
Mutate a canvas: add a node, add an edge, or remove a node. The `op` field selects the mutation. `add-node` needs `nodeType` (`text` for inline markdown or `file` for an embedded note), `content`, `x`, `y` (plus optional `width`/`height`). `add-edge` needs `fromNode` and `toNode` ids (plus optional `label`). `remove-node` needs `nodeId` — removing a node also removes every edge incident to it (destructive). Returns a standard mutation envelope. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Add a text node to a canvas: ```json { "op": "add-node", "filePath": "Boards/map.canvas", "nodeType": "text", "content": "Research question", "x": 0, "y": 0, "width": 280, "height": 80 } ``` Example 2 — Connect two existing nodes with a labelled edge: ```json { "op": "add-edge", "filePath": "Boards/map.canvas", "fromNode": "n1", "toNode": "n2", "label": "depends on" } ``` Example 3 — Remove a node and all its edges: ```json { "op": "remove-node", "filePath": "Boards/map.canvas", "nodeId": "n3" } ```
templates.list
List markdown templates in the vault's templates folder (or a folder of your choosing via `templateFolder`). Use this to discover what templates are available before calling `templates.use`. Read-only. Only returns markdown (`.md`) files. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins.
templates.use
Render or apply a template using one of two engines. The `engine` field selects the engine and the `action` field selects the operation: - `engine:'filesystem'` — kObsidian's built-in `{{variable}}` substitution; no Obsidian plugin required. Actions: `render` (return the expanded text) or `create-note` (write a new note from the template). - `engine:'templater'` — delegate to the Templater Obsidian plugin via the Local REST API. Requires OBSIDIAN_API_URL and OBSIDIAN_REST_API_KEY. Actions: `render` (execute the template and return output), `create-note` (execute and write to `targetFile`), or `insert-active` (insert into the currently active note in Obsidian). The `filesystem` engine is pure text substitution — it does NOT evaluate Templater's `<% … %>` scripts. Use `engine:'templater'` when you need dynamic evaluation. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Render a filesystem template to text (no file written): ```json { "engine": "filesystem", "action": "render", "templatePath": "Templates/daily.md", "variables": { "date": "2026-04-24", "topic": "kObsidian release planning" } } ``` Example 2 — Create a new note from a filesystem template: ```json { "engine": "filesystem", "action": "create-note", "templatePath": "Templates/daily.md", "targetPath": "Journal/2026-04-24.md", "variables": { "date": "2026-04-24" } } ``` Example 3 — Use Templater to create a note via the Obsidian plugin: ```json { "engine": "templater", "action": "create-note", "templateFile": "Templates/meeting.md", "targetFile": "Meetings/Kickoff.md", "openFile": true } ```
workspace.activeFile
Return information about the file currently open and focused in Obsidian — its path, modification time, and whether it's in edit or preview mode. Read-only. Requires the Local REST API plugin (OBSIDIAN_API_URL/OBSIDIAN_REST_API_KEY). Use this to orient the agent before issuing other workspace-level mutations. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing.
workspace.openFile
Open a vault-relative note `filePath` in the live Obsidian UI. `newPane:true` opens it in a new split; default reuses the active pane. UI-only — does not create, modify, or read file contents (use `notes.read` for content). Returns `{ ok: true }` on success; errors when the file does not exist or the Local REST API plugin (OBSIDIAN_API_URL / OBSIDIAN_REST_API_KEY) is unreachable. The opened file targets the live Obsidian process's vault, which may differ from the filesystem session vault — see `vault.current`. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing. Examples: Example 1 — Reveal a daily note in the current pane.: ```json { "filePath": "Daily/2026-04-25.md" } ``` Example 2 — Open a reference note in a side split.: ```json { "filePath": "wiki/Concepts/grpc.md", "newPane": true } ```
workspace.closeActiveFile
Close whatever file is currently active in the Obsidian UI. UI-only — does not delete, save, or modify file contents. No-op when no file is active. Returns `{ ok: true }` on success; errors when the Local REST API plugin is unreachable. Use after `workspace.openFile` when you want to dismiss a temporarily-revealed note. Pair with `workspace.activeFile` first if you need to know what was closed. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing. Examples: Example 1 — Dismiss the currently active pane.: ```json {} ```
workspace.navigate
Navigate the Obsidian back/forward file history, like the arrow buttons in the top-left. `direction:'back'` = back one step; `direction:'forward'` = forward one step. No-op when the stack is empty in the given direction. Requires the Local REST API plugin. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing.
workspace.toggleEditMode
Flip the active file in Obsidian between edit (source) mode and preview (reading) mode. Takes no arguments — always toggles whichever mode is currently active. UI-only: does not modify file contents. No-op when no file is active. Returns `{ ok: true, mode: 'edit' | 'preview' }` reflecting the new mode; errors when the Local REST API plugin is unreachable. Useful when an agent has finished a multi-step edit and wants the user to see the rendered result. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing. Examples: Example 1 — Flip the active note from edit to preview (or vice versa).: ```json {} ```
commands.execute
Execute an Obsidian command by its internal id (as returned by `commands.list`). `args` is an optional argument map passed to the command (most built-in commands take no arguments). Requires the Local REST API plugin. Destructive — the effect depends entirely on what the command does, so verify the command id before calling. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing.
commands.list
List Obsidian commands. With no `query`, returns every registered command (both built-in and plugin-provided). With a `query` string, returns commands whose id or display name matches — substring match, case-insensitive. Read-only. Use this to discover command ids before calling `commands.execute`. Requires the Local REST API plugin. Targets the vault the live Obsidian process has open via the Local REST API. Not affected by `vault.select` — that only changes filesystem-tool routing.
wiki.init
Scaffold the LLM-Wiki layout under the vault: creates `Sources/`, `Concepts/`, `Entities/` folders and seeds `index.md`, `log.md`, and `wiki-schema.md` (the schema reference the agent reads back later). Use this once per vault before calling any other `wiki.*` tool. Idempotent by default — existing files are preserved; pass `force:true` to re-seed `index.md`/`log.md`/`wiki-schema.md` (folders are never deleted). Returns `{ created: string[], skipped: string[] }` so the agent can confirm what changed. Resolves the wiki location from `wikiRoot` arg → `KOBSIDIAN_WIKI_ROOT` env → `wiki/`. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — First-time scaffold in the active vault.: ```json {} ``` Example 2 — Re-seed schema/index/log files in a custom wiki directory.: ```json { "wikiRoot": "knowledge", "force": true } ```
wiki.ingest
File one new source into the wiki: writes `Sources/<slug>.md` with canonical frontmatter, appends an `ingest` entry to `log.md`, and returns a `proposedEdits` array the agent applies via existing `notes.*` tools (`createStub` → `notes.create`; `insertAfterHeading` / `append` → `notes.edit` with the matching `mode`). Cross-reference writes are deliberately NOT applied here so every edit shows up in the transcript. Provide either `sourcePath` (existing vault note) OR `content` (inline markdown) — never both. Use `wiki.summaryMerge` instead when you want to file a follow-up section into an EXISTING concept/entity page; use `wiki.query` to look something up without writing. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Ingest a paper from inline markdown with two related concepts and one entity.: ```json { "title": "In-Context Learning — A Survey", "content": "# In-Context Learning\n\nA survey of …", "sourceType": "paper", "url": "https://arxiv.org/abs/2301.00234", "tags": [ "icl", "prompting" ], "relatedConcepts": [ "In-Context Learning", "Few-Shot Prompting" ], "relatedEntities": [ "Brown 2020" ] } ``` Example 2 — Ingest an existing vault note as a 'note' source.: ```json { "title": "ADR-004 — gRPC for internal service comms", "sourcePath": "drafts/adr-004.md", "sourceType": "note", "tags": [ "adr", "architecture" ] } ```
wiki.logAppend
Append one typed entry to `wiki/log.md` in the canonical format `## [YYYY-MM-DD] <op> | <title>`, optionally followed by a body and a `Refs:` list. The format is chosen so `grep '^## \[' log.md | tail -20` is a valid 'recent activity' query. Use this when the agent makes a wiki-meaningful action that no other `wiki.*` tool already logs (e.g. a `decision` or `note`); `ingest` and `merge` log themselves. Auto-runs `wiki.init` if the wiki has not been scaffolded yet. Idempotent only in the trivial sense — every call appends a new entry. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Log an architectural decision with two refs.: ```json { "op": "decision", "title": "Adopt gRPC for internal RPC", "body": "Streaming + typed schemas outweigh the browser-edge tax.", "refs": [ "wiki/Sources/adr-004.md", "wiki/Concepts/grpc.md" ] } ``` Example 2 — Quick freeform note dated today.: ```json { "op": "note", "title": "Reviewed orphan pages from last sprint" } ```
wiki.indexRebuild
Regenerate `wiki/index.md` from a fresh scan of `Sources/`, `Concepts/`, and `Entities/`. Pages are grouped by category and sorted alphabetically; pass `includeCounts:true` to render counts on the section headings (e.g. `## Sources (12)`). Idempotent and destructive — the existing `index.md` body is replaced wholesale, so any hand-edits there are lost. Use after bulk-creating pages outside the wiki tools, or as the cleanup step after `wiki.lint` reports `indexMismatch`. For incremental upkeep on a single source, prefer the `proposedEdits` returned by `wiki.ingest` instead. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Plain rebuild.: ```json {} ``` Example 2 — Rebuild with counts on each section heading.: ```json { "includeCounts": true } ```
wiki.query
Rank wiki pages by relevance to a free-text `topic`, scanning Sources/Concepts/Entities pages. Hits are weighted in this order: filename match > frontmatter aliases > frontmatter tags > frontmatter summary > body. Returns up to `limit` pages (default 10, max 50) as `{path, type, score, hitFields}` so the agent can drill into the strongest candidates with `notes.read`. Read-only; never writes. Use this for 'what does the wiki know about X?' lookups; use `wiki.lint` instead for whole-vault health audits, and `notes.search` for raw full-text search outside the wiki layout. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Top 10 pages relevant to 'memex vs hypertext'.: ```json { "topic": "memex vs hypertext" } ``` Example 2 — Top 25 pages on a narrow topic, custom wiki dir.: ```json { "topic": "circuit breaker pattern", "limit": 25, "wikiRoot": "knowledge" } ```
wiki.lint
Read-only health check across the wiki. Returns grouped findings under fixed keys: `orphans` (pages with zero in/out wiki-links), `brokenLinks` (links whose target does not resolve), `staleSources` and `stalePages` (older than `staleDays`, default 180 / `KOBSIDIAN_WIKI_STALE_DAYS`), `missingPages` (concept/entity names referenced from Sources but with no page), `tagSingletons` (tags used by exactly one page — likely typos), and `indexMismatch` (entries in `index.md` that no longer match disk). Each group includes a count plus per-finding details. Never writes. Use periodically; pair the result with `notes.move`/`notes.edit`/`wiki.indexRebuild` to apply fixes. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Default audit.: ```json {} ``` Example 2 — Stricter staleness threshold (90 days) for an active codebase wiki.: ```json { "staleDays": 90 } ```
wiki.summaryMerge
Add a cited section to an EXISTING `Concepts/` or `Entities/` page, or create the page with canonical frontmatter if `targetPath` does not exist. The new section is rendered under `heading` (default: `Update YYYY-MM-DD`); `citationSource` adds a `[[wiki-link]]` to the source and pushes it onto the page's `sources:` frontmatter list, and `citationQuote` renders as a blockquote under the citation. On existing pages, `updated:` frontmatter is bumped to today. Use this when filing a follow-up onto a known page; use `wiki.ingest` instead when bringing in a NEW source (which auto-creates `Sources/<slug>.md`). When creating a new entity page, `entityKind` is required. Operates on the session-active vault (see `vault.current` — selectable via `vault.select`) unless an explicit `vaultPath` argument is passed, which always wins. Examples: Example 1 — Append a 'Notable Facts' section to an existing concept page, citing one source with a quote.: ```json { "targetPath": "wiki/Concepts/circuit-breaker.md", "heading": "Notable Facts", "newSection": "Adopted by payment-service after the 2026-04-10 cascade incident.", "citationSource": "wiki/Sources/postmortem-2026-04-10-payment-timeouts-cascade.md", "citationQuote": "Timeouts in payment-service propagated to order-service within 14s." } ``` Example 2 — Create a new entity page for an organization on first reference.: ```json { "targetPath": "wiki/Entities/anthropic.md", "pageType": "entity", "entityKind": "org", "newSection": "AI safety lab; publisher of the Model Context Protocol.", "summary": "AI safety company behind Claude and MCP." } ```
system.version
Return the running kObsidian server's package name, semver version, host runtime (`bun` or `node`), and runtime version. Use this as a health-check or to confirm which server build a client is talking to. Read-only; zero side effects.
Claude Desktop / Cursor
Paste into your MCP client config file to install this server.
{
"mcpServers": {
"kobsidian": {
"kobsidian": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"kobsidian-mcp"
],
"env": {
"OBSIDIAN_VAULT_PATH": "/absolute/path/to/vault",
"OBSIDIAN_API_URL": "https://127.0.0.1:27124",
"OBSIDIAN_API_VERIFY_TLS": "false",
"OBSIDIAN_REST_API_KEY": "only-if-you-use-workspace-or-commands-tools"
}
}
}
}
}
McpServers
{
"kobsidian": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"kobsidian-mcp"
],
"env": {
"OBSIDIAN_VAULT_PATH": "/absolute/path/to/vault",
"OBSIDIAN_API_URL": "https://127.0.0.1:27124",
"OBSIDIAN_API_VERIFY_TLS": "false",
"OBSIDIAN_REST_API_KEY": "only-if-you-use-workspace-or-commands-tools"
}
}
}
kObsidian MCP
Filesystem-first MCP server for Obsidian vaults — with an LLM-Wiki layer on top. _Inspired by Andrej Karpathy's LLM Wiki idea._ You curate the sources; the LLM does the bookkeeping. <br />Why kObsidian
- Filesystem-first. Operates on your vault directly. Obsidian doesn't need to be running for 80+ of the 90 tools. - 90 typed MCP tools across notes, links, tags, tasks, Dataview, Canvas, Kanban, Mermaid, Marp, Templates — every one Zod-validated withstructuredContent output and client-safety annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint).
- LLM-Wiki orchestration — a wiki. namespace that turns your vault into a compounding knowledge base: ingest sources, auto-update an index + greppable log, lint for orphans / broken links / stale pages. Agent applies cross-refs via a proposedEdits contract so every write is visible in the transcript.
- Both transports. Classic stdio for local MCP clients and Streamable HTTP (Hono) for remote, with CORS preflight, MCP-Protocol-Version handling, origin 403, and optional bearer auth — all per the 2025-11-25 spec.
- Ships everywhere. npm (npx -y kobsidian-mcp), cross-platform .mcpb bundles for Claude Desktop drag-and-drop, a smithery.yaml for Smithery, and a server.json for the MCP Registry. Each .mcpb release asset is VirusTotal-scanned with links appended to the release body.
---
Install
Pick the channel that matches your client. All three read the sameOBSIDIAN_VAULT_PATH, OBSIDIAN_API_URL, and OBSIDIAN_REST_API_KEY
environment variables — see docs/ENVIRONMENT.md.
npx / bunx — Claude Code, Claude Desktop, Cursor, VSCode, Antigravity, Zed, Cline, JetBrains AI, …
``json
{
"mcpServers": {
"kobsidian": {
"type": "stdio",
"command": "npx",
"args": ["-y", "kobsidian-mcp"],
"env": {
"OBSIDIAN_VAULT_PATH": "/absolute/path/to/vault",
"OBSIDIAN_API_URL": "https://127.0.0.1:27124",
"OBSIDIAN_API_VERIFY_TLS": "false",
"OBSIDIAN_REST_API_KEY": "only-if-you-use-workspace-or-commands-tools"
}
}
}
}
`
"type": "stdio" is optional on clients that infer transport from
command (Claude Code), but required by Claude Desktop, Cursor,
VSCode, and Antigravity — include it for maximum portability. Swap
npx for bunx for ≈10 ms cold-start instead of ≈200 ms.
Claude Desktop — drag-and-drop
.mcpb
Download kobsidian-<platform>.mcpb from the
latest release and
drag it into Claude Desktop. The installer prompts for vault path +
optional API URL / key. Every release asset is scanned — the VirusTotal
links are in the release body.
Build one locally:
`bash
bun install
bun run build:compile # → dist/kobsidian (or .exe on Windows)
bun run bundle:mcpb # → kobsidian.mcpb
`
Smithery
smithery.ai renders an install UI straight from
smithery.yaml and collects the three env vars for you.
From source (contributing / hacking)
`bash
git clone https://github.com/bezata/kObsidian
cd kObsidian
bun install
bun run dev:stdio # or dev:http
`
---
Obsidian plugins
kObsidian is filesystem-first — 80+ of the 90 tools work against a
bare vault directory with no Obsidian plugins installed. The plugins
below only matter if you want the specific tool namespaces that depend
on them.
Enabling community plugins (one-time, if not already on)
Obsidian ships with community plugins disabled by default. Enable them
once per vault:
1. Open your vault in Obsidian.
2. Settings (⚙️, bottom-left) → Community plugins.
3. Click Turn on community plugins.
4. Browse → search → Install → Enable.
Required for the REST-bridged tools
Obsidian Local REST API (by Adam Coddington) — needed for:
- workspace. (activeFile, openFile, navigateBack/Forward, toggleEditMode)
- commands. (execute, list, search)
- dataview.query / dataview.listByTag / dataview.listByFolder / dataview.table (runtime DQL — the offline dataview.fields. / dataview.index.read / dataview.query.read tools work without it)
- templates.renderTemplater / templates.createNoteTemplater / templates.insertTemplater
Setup after install:
1. Enable the plugin.
2. Open its settings — scroll to API key → click Copy (or Reset first if you want a fresh one).
3. Paste that key as OBSIDIAN_REST_API_KEY in your MCP client config's env: block. The OBSIDIAN_API_URL default (https://127.0.0.1:27124) works out of the box.
Leave the plugin running while you use the REST-bridged tools — the endpoint is local-only (127.0.0.1) so nothing leaves your machine.
Enhances (but not required for) specific tool namespaces
| Plugin | Link | What it unlocks |
|---|---|---|
| Dataview | id=dataview | All dataview. tools still work on the raw markdown; Dataview plugin is what makes DQL queries in dataview.query actually execute. Also renders your fields + queries visually inside Obsidian. |
| Templater | id=templater-obsidian | Runtime template rendering via the REST API (templates.renderTemplater, etc.). The offline templates.expand / templates.list / templates.createNote work without it. |
| Marp | id=marp-slides | Marp marp. tools parse + edit Marp-front-matter markdown even without the plugin; the plugin is what renders slides / exports to PDF inside Obsidian. |
| Kanban | id=obsidian-kanban | kanban. tools read/write the plain markdown board format regardless of plugin; the plugin is what renders the board as draggable columns inside Obsidian. |
| Tasks | id=obsidian-tasks-plugin | tasks. tools understand the Tasks-plugin emoji syntax (📅 ⏳ 🛫 ✅ 🔼 🔁) regardless of plugin; the plugin is what provides filtering / querying / toggling inside Obsidian. |
> The obsidian://show-plugin?id=… links jump straight to the plugin
> in Obsidian's in-app browser — click one with Obsidian open and it
> deep-links to the install screen.
TLDR
| You want to … | Minimum you need |
|---|---|
| Use notes. / tags. / links. / stats. / tasks. / wiki. / kanban. / mermaid. / marp. / canvas. / templates.expand + list + createNote / offline dataview. | Just a vault path. No plugins required. |
| Use workspace. / commands. | + Local REST API plugin + API key env var |
| Run live DQL queries (dataview.query / dataview.listBy / dataview.table) | + Local REST API + Dataview |
| Run Templater templates at runtime | + Local REST API + Templater |
No combination of plugins makes kObsidian depend on Obsidian being
running — the REST-bridged tools just return a clear error if the
plugin isn't reachable, and the filesystem-first tools keep working.
---
Quick start
> Before the first session — kObsidian works on a bare Obsidian vault,
> but enabling a few Obsidian plugins unlocks the full tool surface.
> See Obsidian plugins below for the 5-minute
> setup (Local REST API, Dataview, Templater, Marp, Kanban, Tasks).
> Skip it if you only need the 80+ filesystem-first tools.
Once installed, a typical session opens with three natural-language
prompts. The wiki. tools + the .claude skills handle the rest.
`
You: "Set up a wiki in this vault."
LLM: wiki.init → wiki/{Sources,Concepts,Entities}/ + index.md + log.md + wiki-schema.md
You: "Ingest this: https://… (paper on Memex)"
LLM: wiki.ingest → creates wiki/Sources/as-we-may-think.md + log entry
returns proposedEdits:
- insertAfterHeading index.md#Sources
- createStub Concepts/memex.md
- createStub Entities/vannevar-bush.md
LLM applies each via notes. (you see every write in the transcript)
You: "What does the wiki say about memex vs hypertext?"
LLM: wiki.query memex → top-ranked pages
notes.read on each → cited synthesis
offers to file the synthesis back via wiki.summaryMerge
You: "Audit the wiki."
LLM: wiki.lint → {orphans, brokenLinks, stale, missingPages, tagSingletons, indexMismatch}
proposes concrete fixes; applies after you confirm
`
Full loop, frontmatter contracts, and the proposedEdits design in
docs/wiki.md.
---
Example use cases
The same primitives cover several real-world flavors of knowledge base.
Three worked examples below; longer walkthroughs in docs/examples.md.
A. Personal research wiki
`
You: "Ingest this paper on in-context learning: <url or pasted markdown>"
LLM: wiki.ingest title="In-Context Learning — A Survey" sourceType=paper
tags=[icl, prompting] relatedConcepts=[In-Context Learning, Few-Shot Prompting]
relatedEntities=[Brown 2020]
→ wiki/Sources/in-context-learning-a-survey.md
→ proposedEdits:
• createStub wiki/Concepts/in-context-learning.md
• createStub wiki/Concepts/few-shot-prompting.md
• createStub wiki/Entities/brown-2020.md
• insertAfterHeading wiki/index.md#Sources
LLM applies each via notes.create / notes.insertAfterHeading.
`
B. Architecture Decision Records (ADRs) for a codebase
Model each ADR as a Source, architectural patterns as Concepts, and
services / teams / libraries as Entities. The wiki becomes your ADR
archive with cross-links you never have to maintain by hand.
`
You: "Record ADR-004: we're switching internal service comms from REST
to gRPC. Context: <paste>"
LLM: wiki.ingest title="ADR-004 — gRPC for internal service comms"
sourceType=note tags=[adr, architecture, rpc]
relatedConcepts=[gRPC, Service Mesh, Internal RPC]
relatedEntities=[order-service, payment-service, inventory-service]
→ wiki/Sources/adr-004-grpc-for-internal-service-comms.md
→ proposedEdits:
• createStub wiki/Concepts/grpc.md
• createStub wiki/Concepts/service-mesh.md
• insertAfterHeading wiki/Entities/order-service.md#Notable Facts
• insertAfterHeading wiki/Entities/payment-service.md#Notable Facts
• …
Three weeks later —
You: "Why did we pick gRPC for internal comms?"
LLM: wiki.query "grpc internal comms"
notes.read top matches
→ "Per [[wiki/Sources/adr-004-grpc-for-internal-service-comms.md|ADR-004]],
chosen over REST because of native streaming + typed schemas; tradeoff
accepted: browser clients still use REST via an edge gateway
([[wiki/Concepts/service-mesh.md]])."
`
C. Codebase wiki (design docs + post-mortems + RFCs)
Engineering teams abandon wikis because nobody updates them. Let the
LLM do it. Ingest design docs, RFCs, and post-mortems as Sources;
architectural patterns become Concepts; services and teams become
Entities.
`
You: "We had an incident today — payment-service timeouts cascaded
into order-service. Here's the post-mortem: <paste>"
LLM: wiki.ingest title="Postmortem 2026-04-10 — Payment timeouts cascade"
sourceType=other tags=[postmortem, incident, reliability]
relatedConcepts=[Circuit Breaker, Cascade Failure, Timeout Budget]
relatedEntities=[payment-service, order-service]
→ wiki/Sources/postmortem-2026-04-10-payment-timeouts-cascade.md
→ proposedEdits:
• createStub wiki/Concepts/circuit-breaker.md
• createStub wiki/Concepts/cascade-failure.md
• insertAfterHeading wiki/Entities/payment-service.md#Notable Facts
• insertAfterHeading wiki/Entities/order-service.md#Notable Facts
Periodic housekeeping —
You: "Audit the codebase wiki."
LLM: wiki.lint
→ 3 orphan RFCs (unlinked from any Concept; link or archive?)
→ 1 broken link: [[wiki/Entities/legacy-auth-service.md]]
(deprecated in Q1; remove the link from
[[wiki/Sources/adr-002-session-migration.md]]?)
→ 4 post-mortems past the 180-day stale threshold — tag with
"needs-review" or re-ingest with updated lessons-learned?
→ 2 tag singletons: retry-logic (merge into retry-policy?),
observability (first use; keep).
`
Why this works for engineering teams
- The proposedEdits contract means every cross-reference write is
visible in the transcript — no silent vault corruption from an LLM
hallucination about which services a decision affects.
- The greppable log format (## [YYYY-MM-DD] ingest | ADR-004 …) makes
grep '^## \' wiki/log.md | tail -20 a valid "what did the team
decide recently" query.
- wiki.lint surfaces broken links to services that were deprecated
months ago — the bookkeeping humans never get around to.
---
Architecture
`
┌──────────────────────────────────────────────────────────────────────┐
│ MCP Clients │
│ Claude Code · Claude Desktop · Cursor · VSCode · Antigravity · Zed │
│ JetBrains AI · Cline · Continue · ChatGPT · Smithery · … │
└────────────────────────────┬─────────────────────────────────────────┘
│ JSON-RPC 2.0 · MCP 2025-11-25
┌───────────────────┴──────────────────────┐
▼ ▼
┌──────────────────┐ ┌─────────────────────────┐
│ stdio transport │ │ Streamable HTTP (Hono) │
│ │ │ + OPTIONS / CORS │
│ │ │ + MCP-Protocol-Version │
│ │ │ + Origin 403 / bearer │
└────────┬─────────┘ └────────┬────────────────┘
│ │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────┐
│ McpServer │
│ ┌────────────┐ ┌─────────────┐ │
│ │ 90 Tools │ │ 4 Resources │ │
│ └────────────┘ └─────────────┘ │
│ ┌────────────┐ ┌─────────────┐ │
│ │ 3 Prompts │ │ structured │ │
│ │ │ │ content │ │
│ └────────────┘ └─────────────┘ │
└────────────┬─────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Domain layer (pure) │
│ notes · links · tags · tasks │
│ dataview · canvas · kanban │
│ mermaid · marp · templates │
│ wiki/ orchestration │
└──────┬─────────────────┬─────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────────┐
│ vault/ (FS) │ │ Obsidian Local REST │
│ authoritative│ │ API plugin (optional)│
└──────────────┘ └──────────────────────┘
`
Full module map in [docs/architecture.md.
---
LLM Wiki (60 seconds)
> The tedious part of maintaining a knowledge base is not the reading or > the thinking — it's the bookkeeping. Humans abandon wikis because the > maintenance burden grows faster than the value. LLMs don't get > bored. kObsidian implements the LLM Wiki pattern from Andrej Karpathy's gist: a persistent, compounding knowledge base the LLM maintains. The vault becomes a private, curated Memex (Vannevar Bush, 1945) where cross-references, log-keeping, and lint are the LLM's job while you focus on curating sources and asking questions. > "Instead of just retrieving from raw documents at query time, the LLM > incrementally builds and maintains a persistent wiki — a structured, > interlinked collection of markdown files that sits between you and the > raw sources." — Andrej Karpathy `
User drops a source
│
▼
┌──────────────────────┐ proposedEdits
│ wiki.ingest │ ─────────────────────┐
└──────────┬───────────┘ │
│ creates 1 file ▼
│ ┌──────────────────────────────┐
▼ │ LLM applies edits via │
wiki/Sources/ │ notes.insertAfterHeading │
<slug>.md │ notes.update │
│ │ notes.create │
│ appends └──────────────────────────────┘
▼
wiki/log.md
Anytime: wiki.query → top pages → notes.read → cited synthesis
Periodic: wiki.lint → orphans · broken · stale · missing · tag-drift
Curate: wiki.summaryMerge — add cited section to concept/entity page
`
Default layout under the vault:
`
wiki/
├── Sources/ per-source summary pages
├── Concepts/ topic / idea pages (LLM-maintained)
├── Entities/ people / places / orgs / works
├── index.md categorized catalog (wiki.indexRebuild)
├── log.md greppable chronological log
└── wiki-schema.md vault-local copy of the contract
`
The key design decision is that wiki.ingest never rewrites
cross-references blindly. It creates exactly one file (the Sources
page), appends one file (log.md), and returns a proposedEdits array
the agent applies with existing notes. tools. Every write is visible
in the transcript — so LLM hallucinations show up as reviewable edits
rather than silent vault corruption.
Full contract in docs/wiki.md.
Claude Code skills
Four skills at skills/ trigger on natural language:
wiki-bootstrap, wiki-ingest, wiki-query, wiki-lint. Copy or
symlink them into ~/.claude/skills/ — see
skills/README.md.
---
Tool surface
90 MCP tools across 12 namespaces. Always-current inventory at
docs/tool-inventory.json.
| Namespace | Count | Highlights |
|---|---:|---|
| notes. | 16 | CRUD · search · move · smart-insert (after heading / block) |
| tags. | 6 | Add · remove · search · analyze |
| links. | 8 | Backlinks · broken · orphans · hubs · graph · health |
| stats. | 2 | Per-note + vault-wide metrics |
| tasks. | 5 | Tasks-plugin format (📅 ⏳ 🛫 ✅ 🔼 🔁) |
| dataview. | 13 | Fields · DQL · DataviewJS (source-preserving edits) |
| mermaid. | 3 | Fenced-block parse / read / update |
| marp. | 5 | Slide-level reads + edits |
| kanban. | 5 | Board + card mutations |
| templates. + canvas. | 10 | Templates · Templater bridge · Canvas nodes/edges |
| workspace. + commands. | 9 | Obsidian REST bridge (only tools needing Obsidian running) |
| wiki. | 7 | init · ingest · log · indexRebuild · query · lint · summaryMerge |
Client-safety annotations (MCP 2025-11-25):
| Hint | Tools |
|---|---:|
| readOnlyHint: true (clients can auto-approve) | 47 |
| destructiveHint: true (clients prompt more firmly) | 6 |
| idempotentHint: true (safe to retry) | 12 |
| openWorldHint: true (reaches outside the vault) | 16 |
MCP resources (URI-addressable; any client can browse without tool
calls):
`
kobsidian://wiki/index wiki/index.md
kobsidian://wiki/log wiki/log.md
kobsidian://wiki/schema wiki/wiki-schema.md
kobsidian://wiki/page/{+path} any Sources/Concepts/Entities page
`
MCP prompts (for clients that don't consume the skills/ files):
ingest-source, answer-from-wiki, health-check-wiki.
Details in docs/tools.md.
---
Configuration
| Env var | Default | Purpose |
|---|---|---|
| OBSIDIAN_VAULT_PATH | — | Required. Absolute path to the vault. |
| OBSIDIAN_API_URL | https://127.0.0.1:27124 | Obsidian Local REST API base; only for workspace. / commands. / dataview.query. |
| OBSIDIAN_API_VERIFY_TLS | false | Set true if you've trusted the REST API's self-signed cert. |
| OBSIDIAN_REST_API_KEY | — | Bearer key for the REST API plugin (if used). |
| KOBSIDIAN_HTTP_HOST | 127.0.0.1 | Bind host for dev:http. |
| KOBSIDIAN_HTTP_PORT | 3000 | Bind port for dev:http. |
| KOBSIDIAN_HTTP_BEARER_TOKEN | — | Optional bearer for the Streamable HTTP transport. |
| KOBSIDIAN_ALLOWED_ORIGINS | http://localhost,http://127.0.0.1 | Comma-separated CORS allowlist. |
| KOBSIDIAN_WIKI_ROOT | wiki | Wiki directory under the vault. |
| KOBSIDIAN_WIKI_SOURCES_DIR | Sources | Per-source summary pages. |
| KOBSIDIAN_WIKI_CONCEPTS_DIR | Concepts | Topic / idea pages. |
| KOBSIDIAN_WIKI_ENTITIES_DIR | Entities | People / places / orgs / works. |
| KOBSIDIAN_WIKI_INDEX_FILE | index.md | Wiki catalog filename. |
| KOBSIDIAN_WIKI_LOG_FILE | log.md | Wiki log filename. |
| KOBSIDIAN_WIKI_SCHEMA_FILE | wiki-schema.md | Seed schema filename. |
| KOBSIDIAN_WIKI_STALE_DAYS | 180 | wiki.lint stale-page threshold. |
Every wiki tool also accepts a per-call wikiRoot override.
---
Docs
| | |
|---|---|
| architecture.md | Stack, module map, layering rules |
| wiki.md | LLM-Wiki contract, loop, frontmatter, lint categories |
| examples.md | Personal research wiki · engineering ADRs · codebase wiki — end-to-end |
| tools.md | Namespace table, annotations, resources, prompts |
| SECURITY.md | Origin/CORS, VirusTotal scans, env hygiene |
| TESTING.md | bun run … commands + coverage |
| ENVIRONMENT.md | Every env var with defaults |
| MIGRATION.md | Upgrade notes |
---
Development
`bash
bun install
bun run typecheck
bun run lint
bun run test # 56 tests across 14 files
bun run build # node-target stdio.js + bun-target http.js
bun run inventory # regenerate docs/tool-inventory.json
`
Project conventions in AGENTS.md.
---
Security & supply chain
- Every .mcpb release asset is VirusTotal-scanned. The Release
workflow uploads each kobsidian-<platform>.mcpb bundle to
VirusTotal via
crazy-max/ghaction-virustotal@v4
right after the release is published, then appends the analysis links
to the release body. Any user installing from a GitHub release can
click through to the public VirusTotal report for their platform's
bundle before they run it — no trust in the maintainer required.
- Transport hardening. Streamable HTTP validates Origin against
an allowlist (403 on mismatch), implements CORS preflight
(OPTIONS /mcp → 204 + Access-Control-), requires or defaults
MCP-Protocol-Version, and supports optional bearer auth via
KOBSIDIAN_HTTP_BEARER_TOKEN. stdio has no network surface.
- Pinned SDK floor. @modelcontextprotocol/sdk@^1.26.0 — mitigates
GHSA-345p-7cg4-v4c7 (cross-client response leak) and
CVE-2026-0621 (UriTemplate ReDoS). This repo pins 1.29.0.
- npm Trusted Publishing. No long-lived NPM_TOKEN is stored in the
repo. GitHub Actions mints a short-lived OIDC token on every tag push
and the npm CLI exchanges it for a one-time publish token scoped to
this exact workflow file (.github/workflows/release.yml on the
bezata/kObsidian repo). Provenance attestations are automatic — every
published version has a cryptographically-linked build statement
pointing at the exact Actions run that produced it. Forks, other
branches, or modified workflow files cannot publish — the OIDC
audience claim won't match.
Full notes in docs/SECURITY.md.
---
Compatibility notes
- Protocol version — 2025-11-25 (current MCP spec). HTTP clients
without MCP-Protocol-Version fall back to 2025-03-26 per spec;
explicit-but-unsupported versions return 400.
- Dataview split — offline tools index frontmatter / inline / list /
task / fenced dataview / fenced dataviewjs blocks. Runtime DQL is
delegated to Obsidian + Dataview through the Local REST API.
DataviewJS is source-preserving but not executed inside this server.
- Mermaid + Marp — source-preserving parse/edit only; rendering is
the client's job.
- SDK floor — @modelcontextprotocol/sdk@^1.26.0 (mitigates
GHSA-345p-7cg4-v4c7 cross-client response leak + CVE-2026-0621
UriTemplate ReDoS). This repo pins 1.29.0`.
---
Credits
- LLM Wiki pattern — Andrej Karpathy's gist. kObsidian is one concrete, filesystem-first TypeScript implementation of the idea. - Memex — Vannevar Bush, _As We May Think_, 1945. The associative-trails concept is what the wiki's cross-reference graph tries to be. - Model Context Protocol — Anthropic + the Agentic AI Foundation. - Obsidian — obsidian.md. The vault format is authoritative; kObsidian respects it, doesn't migrate it.License
MIT — see LICENSE. Contributions welcome; open an issue first for anything non-trivial. <div align="center">Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





