diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8773bd7cd..e95907169 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -134,9 +134,11 @@ Common metadata on all discovery tools (`vault_search`, `vault_search_by_tag`, ` 2. **Read-only mode** — `READONLY_MODE=true` keeps exactly the tools whose own `readOnlyHint` annotation says they don't write; write tools are never advertised to clients rather than rejected at call time. The `memory-review` prompt follows its write tool, and the memory bootstrap is skipped. 3. **Per-tool disabling** — `DISABLED_TOOLS` subtracts individually named tools. Purely subtractive: it cannot re-enable a tool an earlier predicate removed. Unknown names fail the boot (a typo that silently disabled nothing would mislead the operator). -Group modules register through a gated wrapper that skips disabled names and injects each tool's annotations from the registry — annotations are declared once, and a new gating axis is one new predicate, not new branching in every module. Availability-keyed cross-references in tool descriptions and prompts disappear whenever their target does; a small number of durable API-level references (e.g. error-section alternatives naming sibling tools) remain. Search, indexing, and the file watcher are unaffected by `READONLY_MODE` and `DISABLED_TOOLS` — they write to the index database outside the vault, not to the vault itself. +How the served set is built and kept consistent: -The wrapper also builds each tool's input with `createStrictInputObject`, so a parameter the tool doesn't declare is rejected with an error naming the accepted ones; tools with no parameters accept any key. +- **Registration** — group modules register through a gated wrapper that skips disabled names and injects each tool's annotations from the registry, so annotations are declared once and a new gating axis is one new predicate. The wrapper also builds each tool's input with `createStrictInputObject`, so a parameter the tool doesn't declare is rejected with an error naming the accepted ones; tools with no parameters accept any key. +- **Cross-references** — references to other tools in tool descriptions and prompts are availability-keyed, so each disappears whenever its target does. A test disables each tool in turn and checks that no served tool's description or input schema still names it. +- **What gating leaves alone** — search, indexing and the file watcher ignore `READONLY_MODE` and `DISABLED_TOOLS`, because they write to the index database outside the vault, not to the vault itself. ### Vault read/write @@ -166,7 +168,7 @@ The edit tools differ in how they locate the lines they change — by heading, b - **`vault_patch_note`** — heading-targeted, with an optional file-level mode. Four operations: `append`, `prepend`, `replace`, `insert_before`. A no-heading `prepend` whose content starts with a heading reports back when it nested pre-existing leading content inside that heading. - **`vault_replace_in_note`** — exact-text find-and-replace in the note body. - **`vault_delete_span`** — anchor-targeted; removes the matched span of whole lines. -- **`vault_replace_span`** — anchor-targeted; swaps the matched span for new content. +- **`vault_replace_span`** — anchor-targeted; swaps the matched span for new content. Empty lines at the content's start or end join the note's empty lines on that side, and each joined run keeps the wider side's count (`collapseEmptyLineRunsAtEdits` in `lines.ts`). Content made only of line breaks touches both sides, so it joins them into one run of empty lines. - **`vault_insert_at_anchor`** — anchor-targeted; inserts content before or after the anchor line without removing it. The three anchor tools share one resolution rule: a short, case-sensitive substring locates a full line, ambiguity is an error, and `first_match` takes the first match instead. @@ -178,7 +180,7 @@ Every tool handles a note's properties block by the same rules, so a write never - **Reads** accept any block the YAML parser can read. A list or single-value block reads as no properties. Reads that return no properties (`vault_read_note`'s `outline` and `heading` modes and `vault_get_memory`) split the block off without parsing it, and `vault_list_memory_files` lists a file whose block is not valid YAML under its file name, with a warning in the server log. - **Body edits** write the block back as written, with LF line breaks, so they accept any block: the five body-edit tools, both memory writes, and a move that changes links only in note bodies. - **Rewrites** of the properties refuse a block they would lose: invalid YAML, a list, a single value, or a value with an explicit YAML tag. They are `vault_update_properties` merges, `vault_write_note` overwrites, the task tools (which write the note back from its parsed properties), and a move that must change a link inside the block. A move reads a block that is not valid YAML as no properties, as Obsidian does, so it rewrites that note's body links and leaves the links inside the block as they were. -- **Results** are checked unless the write keeps the block as written: a write is refused when the note would open with a block the server could not read or keep. With no properties to write, a body that starts with `---` lines becomes the properties block. This happens with a `vault_write_note` body that starts that way, an edit to a note without a properties block that removes the text above such lines (deleting a heading that sits above them), or a call that removes every property. +- **Results** are checked unless the write keeps the block as written: a write is refused when the note would open with a block the server could not read or keep. With no properties to write, a body that starts with `---` lines becomes the properties block. This happens with a `vault_write_note` body that starts that way (when no properties are passed or kept from an overwritten note, or every key is set to `null`), an edit to a note without a properties block that removes the text above such lines (deleting a heading that sits above them), or a call that removes every property. - **Refusals** carry the server's own message, with the line and column when the parser reports a position. The tool then adds how to fix the note: - For a block already in the vault, repair steps when every tool they name is served: `vault_read_note` and `vault_update_properties` with `replace: true`, plus `vault_patch_note` for a block that may hold prose to put back. Otherwise it points at Obsidian. - For a `vault_write_note` overwrite of such a note, which replaces the body anyway, the steps skip the prose: replace the block, then run the overwrite again. These steps need only `vault_read_note` and `vault_update_properties`; if either is not served, it points at Obsidian. @@ -297,7 +299,9 @@ whole files/sections, or date-scoped entries across them with `on_or_after`; **Auto-initialization:** A two-layer bootstrap — startup seeds the default structure, write-time handles growth beyond it: - **Startup:** if the memory folder (default: `About Me/`) doesn't exist, the server creates it with template files (Me.md, Opinions.md, Principles.md, Routines.md, Agents.md), each opening with a `> [!info] Scope of this file` callout so agents discover a ready, self-documenting structure. -- **Write-time:** `vault_update_memory` auto-creates files and sections on write — agents can save preferences without manual setup; a newly-created file is seeded with a placeholder scope callout to fill in. +- **Write-time:** `vault_update_memory` auto-creates files and sections on write — agents can save preferences without manual setup; a newly-created file is seeded with a placeholder scope callout to fill in, and a newly-created section heading gets "(newest first)" appended. + +**Listing:** `listMemoryFiles` (behind `vault_list_memory_files`) lists the `.md` files directly inside the memory folder, whether or not each has frontmatter or a scope callout. It skips hidden names and names ending in `.md` twice in any letter case (`Opinions.md.md`): `memoryFilePath` refuses both kinds, so no memory tool could reach them by name. **Opt-out:** The memory layer is opt-out: set `MEMORY_ENABLED=false` to hide all memory tools and prompts, skip auto-initialization, and strip memory references from server metadata. `READONLY_MODE=true` also skips auto-initialization (no vault writes at startup). The vault CRUD and search layers continue to work normally. @@ -317,11 +321,12 @@ Link queries use a `links` table populated during indexing: 2. Path relative to the linking note (path from current file, including upward `../`) 3. Basename (shortest-path-first for ambiguous basenames) - **Non-markdown files:** Targets that don't resolve to a note are checked against a `non_md_files` table (populated during rebuild, maintained by the file watcher). Both wikilinks and markdown-style links to `.canvas`, `.base`, images, PDFs, and other non-markdown files resolve as `kind: "file"` instead of being counted as broken. -- **Outgoing links:** `vault_get_outgoing_links` returns a `kind` discriminator (`"note"` or `"file"`) plus each target's byte size (`bytes` — from the notes table for notes, from `non_md_files` for files), so clients can route notes to `vault_read_note` and files to `vault_read_file` with size awareness. +- **Outgoing links:** `vault_get_outgoing_links` returns a `kind` discriminator (`"note"` or `"file"`) plus each target's byte size (`bytes` — from the notes table for notes, from `non_md_files` for files), so clients can route notes to `vault_read_note` and files to `vault_read_file` with size awareness. A broken link keeps the target text the index stored: its original letter case, and for a markdown link, its `%`-escapes decoded. - **Orphans:** `vault_find_orphans` and `vault-orientation` resolve default exclusions on each invocation: the daily notes folder (resolved as [Daily notes](#property-discovery--daily-notes) describes), `Templates`, and the memory dir. - `ORPHAN_EXCLUDE_FOLDERS` replaces that list; a tool call's `exclude_folders` replaces it for that call. - An unreadable or malformed config logs a warning and uses `Daily Notes`; a missing file uses that fallback without a warning. - A vault-root, blank, or whitespace-only folder setting adds no daily-folder exclusion, so those daily notes remain eligible. + - Each excluded folder adds one `NOT LIKE` clause, and SQLite's expression-depth limit caps the list at 994 folders. `findOrphans` (`search-queries.ts`) translates SQLite's "Expression tree is too large" error to "too many excluded folders". ### Files @@ -332,7 +337,7 @@ Link queries use a `links` table populated during indexing: `vault_read_file` reads non-markdown vault files, dispatching on extension to the most useful representation per type: -1. **Images** (`.png`/`.jpg`/`.jpeg`/`.gif`/`.webp`) return an MCP `image` content block plus a one-line metadata text block. A shared fit-to-byte-budget pipeline (`utils/fit-image-to-byte-budget.ts`, built on sharp) makes oversized images deliverable: EXIF auto-orient → resize long edge to ≤1568px → walk a fixed quality ladder (JPEG via mozjpeg for opaque images, WebP for alpha — PNG has no quality knob) → shrink dimensions by √(budget/actual) if the ladder floor still exceeds the budget. Deterministic and terminating (bounded attempts, 64px floor); sharp's default `limitInputPixels` stays active as the decompression-bomb guard. The budget (`MAX_IMAGE_OUTPUT_BYTES`, default 48 KiB binary) is sized for the tightest mainstream client cap. +1. **Images** (`.png`/`.jpg`/`.jpeg`/`.gif`/`.webp`) return an MCP `image` content block plus a one-line metadata text block: the path, the delivered format, dimensions and bytes, and, when the image was recompressed, its original dimensions and bytes. A shared fit-to-byte-budget pipeline (`utils/fit-image-to-byte-budget.ts`, built on sharp) makes oversized images deliverable: EXIF auto-orient → resize long edge to ≤1568px → walk a fixed quality ladder (JPEG via mozjpeg for opaque images, WebP for alpha — PNG has no quality knob) → shrink dimensions by √(budget/actual) if the ladder floor still exceeds the budget. Deterministic and terminating (bounded attempts, 64px floor); sharp's default `limitInputPixels` stays active as the decompression-bomb guard. The budget (`MAX_IMAGE_OUTPUT_BYTES`, default 48 KiB binary) is sized for the tightest mainstream client cap. 2. **Canvas** (`.canvas`) linearizes to markdown via the pure `obsidian-markdown/canvas.ts` parser ([JSON Canvas 1.0](https://jsoncanvas.org)): group membership by spatial rect containment (innermost group wins; equal rects tiebreak deterministically by id), nodes in reading order (y, then x), and an edge list with node ids resolved to display names. Lenient parsing — unknown properties ignored, malformed entries skipped. `raw: true` skips the linearizer and returns the JSON source verbatim for full structural fidelity. Canvas content is also FTS-indexed (linearized text in `file_content` + `file_content_fts` tables, gated behind `FILE_TOOLS_ENABLED`) so canvas files appear in `vault_search` results with `kind: "file"`. Canvas `file`-type node references are extracted unconditionally into the `links` table for graph queries regardless of the feature gate. 3. **Text formats** (`.svg`/`.json`/`.txt`/`.csv`/`.xml`/`.log`/`.yaml`/`.yml`/`.base`) pass through verbatim as text, capped at a fixed 100 KiB output size (explicit error over silent truncation). `start_line`/`limit` page any text result — passthrough formats, canvas renditions, PDF-extracted text — as a 1-based line window preceded by a window-metadata block stating the range, total line count, and next `start_line`; the cap applies to the window, and an unpaged read stays byte-exact. 4. **PDFs** (`.pdf`) return structured markdown reconstructed from layout-aware extraction (`unpdf`, based on Mozilla's PDF.js): @@ -351,7 +356,7 @@ Link queries use a `links` table populated during indexing: 5. **Unknown types** return an error naming the readable set. -The extension-to-representation routing above is implemented by the `vault-operations/asset-operations.ts` use-case. Beneath it, every read goes through `vaultFs.readAsset`, which applies the same `resolveSafePath` traversal + hidden-path guards as notes, rejects `.md` paths (notes belong to `vault_read_note`), and enforces a stat-before-read size cap (`MAX_FILE_BYTES`, default 50 MiB). +The extension-to-representation routing above is implemented by the `vault-operations/asset-operations.ts` use-case. Beneath it, every read goes through `vaultFs.readAsset`, which applies the same `resolveSafePath` traversal + hidden-path guards as notes, rejects `.md` paths (notes belong to `vault_read_note`), and enforces a stat-before-read size cap (`MAX_FILE_BYTES`, default 50 MiB). The read buffer is one byte longer than the statted size, so a file that grows during the read (an Obsidian Sync download can do this) fills that byte, and the read fails with "file changed while reading". `vault_list_files` is the discovery surface (also `vault-operations/asset-operations.ts`): a filesystem walk (`vaultFs.listAssets` — filesystem truth, deliberately not the index), folder and case-insensitive extension filters, per-extension counts computed over the full filtered set, and byte sizes statted only for the returned slice. Canvas, PDF, and text files (.txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base) are indexed for full-text search in the `file_content` + `file_content_fts` tables — see Canvas entry above for the canvas-specific link graph integration. PDF text is extracted via `obsidian-markdown/pdf.ts` (structured markdown reconstruction) — this works on PDFs with embedded text content; scanned or image-only PDFs produce no indexable text and are silently skipped. Text files are indexed as raw UTF-8 content. Content exceeding 100 KiB is truncated before FTS insertion. @@ -405,6 +410,8 @@ Four design choices shape the query surface: Both tools refuse a `block_id` found at the end of more than one task line, as the task to update or as a new sub-task's parent, and list every matching line instead of acting on the first. +Both tools parse the written line back (`diffTaskRoundTrip` in `tasks.ts`) and return an advisory, not an error, for each field that does not read back as the call intended. Description or checklist text that ends in a signifier is read back according to that field's value grammar: a trailing `🔁` reads any following words as its recurrence rule, while `📅` followed by words that are not a `YYYY-MM-DD` date stays description text. + `vault_create_task` and `vault_update_task` read the Tasks plugin's format and date toggles on every write, so a change in Obsidian applies to the next write. The plugin's status registry is read once at boot, so `vault_list_tasks` and the two write tools always agree on which checkboxes are tasks; a status-type change needs a restart. ## MCP Prompts @@ -1241,15 +1248,28 @@ Docker hardening, and durability seatbelts above. deleting, it checks that the file still sits inside `.trash/` and is still the unchanged file it trashed, so a corrupted record, a symlink, or a note Obsidian later trashed under the same name is left alone. + - A name already taken in `.trash/` gets Obsidian's suffixes (`Plan 1.md`, + `Plan 2.md`, …); after `TRASH_NAME_SUFFIX_LIMIT` (100) numbered names the + delete fails. + - The setting is read from `.obsidian/app.json` on every delete + (`trash-config.ts`), except on a server running Obsidian Sync, where a + delete is always permanent and Sync's version history is the recovery + path. A missing file, or a missing or unknown `trashOption`, means + `system`; a file that exists but cannot be read or parsed refuses the + delete, because a `system` fallback could let the sweep remove a note + the user set to keep. - **Verify-then-preflight-then-commit move** (`note-mover.ts`): under the lock, `moveNote` first scans notes and Canvas files for backlinks the search index missed (closing a lag race) or never records (links in Canvas text cards). The scan skips a file it cannot read, or a Canvas file whose JSON it cannot parse, with a warning, so such a file aborts the move only when the index lists it as a backlink. A note whose - properties block is not valid YAML is scanned for its body links. The move then reads every affected file and - computes every rewrite _before_ touching anything. If any read fails, no - file is mutated. Destination is written first via exclusive create; + properties block is not valid YAML is scanned for its body links. When + the scan finds new linking notes, the move releases the lock, adds them + to the lock set and scans again, up to `MAX_BACKLINK_VERIFY_RETRIES` (3) + retries; if new ones still appear, it aborts with nothing written. The + move then reads every affected file and computes every rewrite _before_ + touching anything. If any read fails, no file is mutated. Destination is written first via exclusive create; source is deleted last — a failure at any step leaves both copies rather than losing content. - **Content-hash gating** (`search-index.ts`): SHA-256 per chunk. Only @@ -1348,6 +1368,11 @@ Docker hardening, and durability seatbelts above. `pdf_page_render_failed` logs, and the logs of PDFs the search index or file watcher cannot read, keep the full message and the cause of a wrapped error (`describeErrorForLog`). +- **Memory file read failures** (`memory-store.ts`): a failed read of a + memory file is rethrown as "cannot read memory file …" with the + vault-relative path and no Node error code, so the tool boundary adds no + coded remedy. A file name longer than the filesystem allows surfaces + under this message, not as `ENAMETOOLONG`. - **In-lock existence checks**: `deleteNote` and `moveNote` check file existence inside the lock, so a missing note returns the "note not found" error the tools document instead of Node's ENOENT text. @@ -1374,6 +1399,12 @@ Docker hardening, and durability seatbelts above. - **Idempotency guard**: if the exact bullet already exists in the target section, `updateMemory` no-ops — prevents duplicate entries from MCP client retries after gateway timeouts. +- **Near-duplicate section guard** (`findNearMissSection` in + `memory-store.ts`): `updateMemory` refuses to create a section whose name + differs from an existing H2 heading only by an HTML-entity slip, a typo + within a length-scaled edit budget, or spacing ("section not created"), + so entries do not split across near-identical headings. Names + that differ only in their digits ("2025" vs "2026") count as different. - **Line-break rejection**: entry text, date, and section name all reject `\r` and `\n` — a multiline entry would corrupt the dated-bullet format and evade the duplicate guard. diff --git a/SECURITY.md b/SECURITY.md index 0cf9cec37..10715a020 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -106,10 +106,10 @@ mechanism-level detail. index, OAuth database) remain. - `DISABLED_TOOLS` narrows the surface tool-by-tool — e.g. keep writes on but remove the delete tools. Same registration-time guarantee. -- Availability-keyed cross-references in surviving descriptions and - prompts disappear automatically. A small number of durable API-level - references remain (e.g. error-section alternatives naming sibling - tools). +- Cross-references in surviving descriptions and prompts are + availability-keyed and disappear with their target. A test disables + each tool in turn and checks that no served tool's description or + input schema still names it. - Unknown tool names stop the server at startup rather than silently disabling nothing. diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 0b9c0cbae..de3da9707 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json @@ -6,7 +6,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -103,7 +103,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -149,7 +149,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -196,7 +196,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -230,7 +230,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -242,10 +242,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -275,7 +275,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -311,7 +311,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -340,7 +340,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -366,7 +366,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -376,12 +376,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -402,7 +402,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -431,7 +431,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -443,7 +443,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -451,7 +451,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -486,7 +486,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -528,7 +528,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -547,7 +547,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -578,7 +578,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -604,7 +604,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -645,7 +645,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -664,13 +664,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -894,7 +894,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -908,7 +908,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -932,7 +932,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -942,7 +942,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -973,7 +973,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -985,7 +985,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -1013,7 +1013,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -1074,7 +1074,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -1119,7 +1119,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1133,7 +1133,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1142,13 +1142,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1179,13 +1179,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1216,7 +1216,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1261,7 +1261,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1273,17 +1273,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1312,7 +1312,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1459,7 +1459,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1470,7 +1470,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1500,7 +1500,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1547,14 +1547,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1588,7 +1588,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1605,7 +1605,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1650,7 +1650,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1694,7 +1694,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1770,7 +1770,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1851,7 +1851,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1877,7 +1877,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1921,7 +1921,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json index 031c78276..ad26c3599 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json @@ -8,7 +8,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -105,7 +105,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -151,7 +151,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -198,7 +198,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -232,7 +232,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -244,10 +244,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -277,7 +277,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -313,7 +313,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -342,7 +342,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -368,7 +368,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -378,12 +378,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -404,7 +404,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -433,7 +433,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -445,7 +445,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -453,7 +453,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -488,7 +488,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -530,7 +530,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -549,7 +549,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -580,7 +580,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -606,7 +606,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -647,7 +647,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -666,13 +666,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -896,7 +896,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -910,7 +910,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -934,7 +934,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -944,7 +944,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -975,7 +975,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's properties, use vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -987,7 +987,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -1015,7 +1015,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -1060,7 +1060,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1074,7 +1074,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1083,13 +1083,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1120,13 +1120,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1157,7 +1157,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1202,7 +1202,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1214,17 +1214,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1253,7 +1253,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1400,7 +1400,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1411,7 +1411,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1441,7 +1441,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1488,14 +1488,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1529,7 +1529,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1546,7 +1546,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1591,7 +1591,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1635,7 +1635,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1711,7 +1711,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1792,7 +1792,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1818,7 +1818,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1862,7 +1862,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json index c85afa09b..877dc63bb 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json @@ -8,7 +8,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -105,7 +105,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -151,7 +151,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -198,7 +198,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -232,7 +232,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -244,10 +244,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -277,7 +277,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -313,7 +313,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -342,7 +342,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -368,7 +368,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -378,12 +378,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -404,7 +404,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -433,7 +433,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -445,7 +445,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -453,7 +453,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -488,7 +488,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -530,7 +530,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -549,7 +549,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -580,7 +580,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -606,7 +606,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -647,7 +647,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -666,13 +666,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -896,7 +896,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -910,7 +910,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -934,7 +934,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", + "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", "inputSchema": { "type": "object", "properties": { @@ -944,7 +944,7 @@ "description": "Topic to recall — use specific keywords." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -975,7 +975,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -987,7 +987,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -1015,7 +1015,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -1076,7 +1076,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -1121,7 +1121,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1135,7 +1135,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1144,13 +1144,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1181,13 +1181,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1218,7 +1218,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1263,7 +1263,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1275,17 +1275,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1314,7 +1314,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1461,7 +1461,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1472,7 +1472,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1502,7 +1502,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1549,14 +1549,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1590,7 +1590,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1607,7 +1607,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1652,7 +1652,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1696,7 +1696,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1772,7 +1772,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1853,7 +1853,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1879,7 +1879,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1923,7 +1923,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json index 026997622..86bc66b9f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -106,7 +106,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -152,7 +152,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -199,7 +199,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -233,7 +233,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -245,10 +245,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -278,7 +278,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -314,7 +314,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -343,7 +343,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -369,7 +369,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -379,12 +379,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -405,7 +405,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -434,7 +434,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -446,7 +446,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -454,7 +454,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -489,7 +489,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -508,7 +508,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -539,7 +539,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -565,7 +565,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -606,7 +606,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -625,13 +625,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -855,7 +855,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -869,7 +869,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -893,7 +893,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", + "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", "inputSchema": { "type": "object", "properties": { @@ -903,7 +903,7 @@ "description": "Topic to recall — use specific keywords." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -934,7 +934,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -946,7 +946,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -974,7 +974,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -1035,7 +1035,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1049,7 +1049,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1058,13 +1058,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1095,13 +1095,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1132,7 +1132,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1177,7 +1177,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1189,17 +1189,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1228,7 +1228,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1375,7 +1375,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1386,7 +1386,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1416,7 +1416,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1463,14 +1463,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1504,7 +1504,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1521,7 +1521,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1566,7 +1566,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1610,7 +1610,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1686,7 +1686,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1767,7 +1767,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1793,7 +1793,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1837,7 +1837,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json index 00266ec36..fbe3c1383 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json @@ -8,7 +8,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -105,7 +105,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -151,7 +151,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -198,7 +198,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -232,7 +232,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -244,10 +244,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -277,7 +277,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -313,7 +313,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -342,7 +342,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -368,7 +368,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -378,12 +378,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -404,7 +404,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -433,7 +433,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -445,7 +445,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -453,7 +453,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -488,7 +488,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -507,7 +507,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -538,7 +538,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -564,7 +564,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -605,7 +605,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -624,13 +624,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -854,7 +854,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -868,7 +868,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -892,7 +892,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -902,7 +902,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -933,7 +933,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -945,7 +945,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -973,7 +973,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -1034,7 +1034,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1048,7 +1048,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1057,13 +1057,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1094,13 +1094,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1131,7 +1131,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1176,7 +1176,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1188,17 +1188,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1227,7 +1227,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1374,7 +1374,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1385,7 +1385,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1415,7 +1415,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1462,14 +1462,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1503,7 +1503,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1520,7 +1520,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1565,7 +1565,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1609,7 +1609,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1685,7 +1685,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1766,7 +1766,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1792,7 +1792,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1836,7 +1836,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json index 5cd32631b..7fa635f40 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -106,7 +106,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; ask the vault owner to delete it in Obsidian\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -186,7 +186,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -198,10 +198,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -231,7 +231,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -267,7 +267,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -296,7 +296,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -322,7 +322,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -351,7 +351,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -363,7 +363,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -371,7 +371,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -406,7 +406,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -448,7 +448,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -479,7 +479,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -505,7 +505,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -546,7 +546,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -565,13 +565,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -795,7 +795,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -809,7 +809,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -833,7 +833,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -845,7 +845,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -873,7 +873,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -934,7 +934,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -979,7 +979,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -993,7 +993,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1002,13 +1002,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1039,13 +1039,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1076,7 +1076,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1121,7 +1121,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1133,17 +1133,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1172,7 +1172,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1319,7 +1319,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1330,7 +1330,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1360,7 +1360,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1407,14 +1407,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1448,7 +1448,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1492,7 +1492,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1568,7 +1568,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1649,7 +1649,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1675,7 +1675,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1719,7 +1719,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json index 38fd9a022..b79fe2aae 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -107,7 +107,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -153,7 +153,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; ask the vault owner to delete it in Obsidian\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -187,7 +187,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -199,10 +199,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -232,7 +232,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -268,7 +268,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -297,7 +297,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -323,7 +323,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -352,7 +352,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -364,7 +364,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -372,7 +372,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -407,7 +407,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -438,7 +438,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -464,7 +464,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -505,7 +505,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -524,13 +524,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -754,7 +754,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -768,7 +768,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -792,7 +792,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -804,7 +804,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -832,7 +832,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -893,7 +893,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -907,7 +907,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -916,13 +916,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -953,13 +953,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -990,7 +990,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1035,7 +1035,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1047,17 +1047,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1086,7 +1086,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1233,7 +1233,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1244,7 +1244,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1274,7 +1274,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1321,14 +1321,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1362,7 +1362,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1406,7 +1406,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1482,7 +1482,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1563,7 +1563,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1589,7 +1589,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1633,7 +1633,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json index 5938f2a34..0485a4b8e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -106,7 +106,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -152,7 +152,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; ask the vault owner to delete it in Obsidian\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -186,7 +186,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -198,10 +198,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -231,7 +231,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -267,7 +267,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -296,7 +296,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -322,7 +322,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -351,7 +351,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -363,7 +363,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -371,7 +371,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -406,7 +406,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -437,7 +437,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -463,7 +463,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -504,7 +504,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -523,13 +523,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -753,7 +753,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -767,7 +767,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -791,7 +791,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -803,7 +803,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -831,7 +831,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -892,7 +892,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -906,7 +906,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -915,13 +915,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -952,13 +952,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -989,7 +989,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1034,7 +1034,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1046,17 +1046,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1085,7 +1085,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1232,7 +1232,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1243,7 +1243,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1273,7 +1273,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1320,14 +1320,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1361,7 +1361,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1405,7 +1405,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1481,7 +1481,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1562,7 +1562,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1588,7 +1588,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1632,7 +1632,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json index 034c0c367..25587b6c3 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json @@ -8,7 +8,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -105,7 +105,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -151,7 +151,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies (\"Plan 1.md\" … \"Plan 100.md\"); ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — the file exists but can't be read or parsed; ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note, moving it to the vault's .trash/ folder or removing it for good as the vault's Obsidian \"Deleted files\" setting directs.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- The \"Deleted files\" setting decides the outcome, which the caller can't choose or see in advance:\n - \"Move to system trash\" (also what an absent setting means) moves the note to `.trash/`. Notes this server moved there are deleted after its retention period (TRASH_RETENTION_DAYS: 30 days by default, never when set to none).\n - \"Move to Obsidian trash\" moves the note to `.trash/` and keeps it forever.\n - \"Permanently delete\" removes the note for good.\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; ask the vault owner to delete it in Obsidian\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot move to trash … — 100 collisions in .trash/\" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry\n- \"cannot move to trash …\" without \"100 collisions\" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read trash config from .obsidian/app.json\" — ask the vault owner to repair it, then retry\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message naming the outcome — \"Deleted \" for permanent removal, \"Moved to trash (<.trash/ path>)\" when the note landed in .trash/. Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -185,7 +185,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -197,10 +197,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -230,7 +230,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -266,7 +266,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -295,7 +295,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -321,7 +321,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -350,7 +350,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -362,7 +362,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -370,7 +370,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -405,7 +405,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -447,7 +447,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -478,7 +478,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -504,7 +504,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -545,7 +545,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -564,13 +564,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -794,7 +794,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -808,7 +808,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -832,7 +832,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -844,7 +844,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -872,7 +872,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -933,7 +933,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -978,7 +978,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -992,7 +992,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1001,13 +1001,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1038,13 +1038,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1075,7 +1075,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1120,7 +1120,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1132,17 +1132,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1171,7 +1171,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1318,7 +1318,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1329,7 +1329,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1359,7 +1359,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1406,14 +1406,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1447,7 +1447,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1491,7 +1491,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1567,7 +1567,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1648,7 +1648,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1674,7 +1674,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1718,7 +1718,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json index c09b15f2a..038dc9f88 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json @@ -8,7 +8,7 @@ { "name": "vault_create_task", "title": "Create Task", - "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request, since the Tasks plugin reads just one format.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- priority: the Tasks plugin ranks \"no signifier\" (normal priority) between medium and low.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n- due / scheduled / start: omit a date rather than guessing.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — kanban-plugin note without heading; pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — drop the block_id, or the trailing ^id from the checklist item\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", + "description": "Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today; set in_progress later with vault_update_task. Override format only at the user's request.\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" }) — a card in a Kanban board's Active lane\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under fix-login\nExample: vault_create_task({ path: \"Projects/plan.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task in a regular note under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Write release notes\", block_id: \"release-notes\", heading: \"Active\", position: 3 }) — insert as the 3rd card in the lane\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering, so the card round-trips through vault_list_tasks with all fields intact. For lightweight checklist items under an existing card (no metadata), use vault_update_task's add_subtasks param instead.\n\nParameters:\n- heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given.\n- parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board.\n- position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy.\n\nErrors: (messages use camelCase names, e.g. blockId for block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"heading required for Kanban boards\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"cannot place at position P under \"X\" — the heading appears N times\" — integer position on a note with duplicate heading names; use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line with vault_list_tasks\n- \"parent task ambiguous\" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server\n- \"parentBlockId and parentLine are mutually exclusive\" / \"parent and heading are mutually exclusive\" — drop one\n- \"blockId ... already exists in this note\" / \"blockId ... contains invalid characters\" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters\n- \"blockId is required\" — pass block_id\n- \"blockId is not allowed on a sub-task on a Kanban board\" / \"subtask ... ends in a block ID\" — the message names what to drop\n- \"description is empty\" / \"subtasks cannot contain an empty item\" / \"description must be a single line\" / \"subtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"invalid date: …\" — the message names the expected format\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as \"field: (none) → value\", and checklist items as \"subtasks: 0 → N\"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.", "inputSchema": { "type": "object", "properties": { @@ -105,7 +105,7 @@ "minLength": 1 }, "depends_on": { - "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", "minItems": 1, "type": "array", "items": { @@ -151,7 +151,7 @@ { "name": "vault_delete_memory", "title": "Delete Memory Entry", - "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md.\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"no entry matching …\" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", + "description": "Delete a single dated entry from a memory file in About Me/.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"Tools and workflows (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\nExample: vault_delete_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", date: \"2026-03-10\", entry: \"Gym Tue/Thu at 7am\" }) — prune an expired entry from an entry-policy: living file\n\nWhen to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead. The exception is a file whose frontmatter declares `entry-policy: living` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching.\nPrefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n\nParameters:\n- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry.\n- section scopes the match — an identical entry under a different heading is not found.\n\nErrors:\n- \"memory file not found\" — call vault_list_memory_files to discover valid names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"date must be YYYY-MM-DD\" — pass the entry's date exactly as the file shows it, zero-padded.\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"no entry matching …\" — verify the exact text with vault_get_memory(file, section).\n- \"ambiguous: N entries match …\" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.\n- \"refusing memory write: … would shrink content\" — the delete would remove more than half the file; re-read it with vault_get_memory, and remove an entry that really is that large with vault_delete_span or a manual edit.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Deleted entry from About Me/.md → ##
\".", "inputSchema": { "type": "object", "properties": { @@ -198,7 +198,7 @@ { "name": "vault_delete_note", "title": "Delete Note", - "description": "Delete a markdown note for good. The vault owner can recover it from Obsidian Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", + "description": "Delete a markdown note for good. The vault owner can recover it from Obsidian Sync's version history (1 month on Standard, 12 months on Plus).\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\nTo relocate a note, use vault_move_note instead.\nTo replace a note's content, use vault_write_note with overwrite: true instead.\n\nBehavior:\n- Links to the note from other notes become broken; list them first with vault_get_backlinks. Protected paths are refused: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — verify path with vault_list_notes\n- \"cannot delete …\" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; the note stays put\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry\n\nReturns: Confirmation message — \"Deleted \". Notes how many empty folders were pruned when any were.", "inputSchema": { "type": "object", "properties": { @@ -232,7 +232,7 @@ { "name": "vault_delete_span", "title": "Delete Span", - "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", + "description": "Delete a contiguous block of whole lines from a note's body by referencing short anchor substrings instead of reproducing the full block text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_delete_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\" }) — deletes the one table row whose line contains that fragment.\nExample: vault_delete_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\" }) — deletes from the start anchor line through the end anchor line.\n\nWhen to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, use vault_replace_span (one atomic step).\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to remove such a block, use vault_replace_in_note with new_text: \"\".\n\nBehavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nReturns: 'Deleted line(s) from : \"\"' — the removed lines joined by line breaks, cut at 80 characters with \"…\".", "inputSchema": { "type": "object", "properties": { @@ -244,10 +244,10 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", "type": "string", "minLength": 1 }, @@ -277,7 +277,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph. Link an orphan by mentioning it from a relevant note with vault_patch_note.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -313,7 +313,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -342,7 +342,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. To append content to a daily note section, use the returned path with vault_patch_note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean). When exists is false, create the note with vault_write_note using the returned path.", "inputSchema": { "type": "object", "properties": { @@ -368,7 +368,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -378,12 +378,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -404,7 +404,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -433,7 +433,7 @@ { "name": "vault_insert_at_anchor", "title": "Insert at Anchor", - "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", + "description": "Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged.\n\nExample: vault_insert_at_anchor({ path: \"Tracker.md\", anchor: \"| 2024-03-02 | Acme\", position: \"after\", content: \"| 2024-03-03 | Beta Corp | New entry |\" }) — inserts a new table row after the matched row.\nExample: vault_insert_at_anchor({ path: \"Notes/Plan.md\", anchor: \"## Phase 2\", position: \"before\", content: \"> [!note] Phase 1 must close before this starts.\\n\" }) — inserts a callout and a blank line above the Phase 2 heading.\n\nWhen to use: Adding content at a precise location identified by a nearby line's text, without needing to know the heading structure. Good for inserting rows into tables, adding items into lists at a specific position, or placing content relative to a known landmark line.\nPrefer vault_patch_note for heading-targeted inserts (append/prepend to a section). Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"anchor not found\" — fragment not on any body line; verify with vault_read_note\n- \"ambiguous anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted line(s) anchor in \" — N counts the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -445,7 +445,7 @@ "anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring on the line to insert next to (case-sensitive). Pick a brief fragment — do not paste the whole line." + "description": "Short, unique substring of the body line to insert next to (case-sensitive)." }, "position": { "type": "string", @@ -453,7 +453,7 @@ "before", "after" ], - "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + "description": "\"before\" inserts above the anchor line, \"after\" below it; the anchor line itself is never changed." }, "content": { "type": "string", @@ -488,7 +488,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -530,7 +530,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry_policy before pruning entries.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory, and each file's entry_policy to check before pruning entries. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -549,7 +549,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -580,7 +580,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -606,7 +606,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -647,7 +647,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -666,13 +666,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -896,7 +896,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -910,7 +910,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -934,7 +934,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory or vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -944,7 +944,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -975,7 +975,7 @@ { "name": "vault_move_note", "title": "Move Note", - "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — old_path and new_path name the same note, so there is nothing to move.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — both old_path and new_path must end in .md.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move.\n- \"move incomplete: …\" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish.\n- A case-only rename that fails partway — \"move aborted: could not rename to …\" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. \"move incomplete: …\" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move.\n- \"move aborted: could not read …\" / \"move aborted: could not rewrite …\" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry.\n- \"move aborted: could not write to …\" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do; nothing was written.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).", + "description": "Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone.\n\nExample: vault_move_note({ old_path: \"Inbox/Draft.md\", new_path: \"Inbox/Spec.md\" }) — pure rename.\nExample: vault_move_note({ old_path: \"Inbox/spec.md\", new_path: \"Inbox/Spec.md\" }) — case-only rename; works even where the filesystem treats both spellings as one file.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\" }) — move to another folder.\nExample: vault_move_note({ old_path: \"Inbox/Spec.md\", new_path: \"Projects/Spec.md\", prune_empty_folders: true }) — also remove \"Inbox\" if the move empties it.\n\nWhen to use: Renaming a note or relocating it to a different folder while keeping the link graph intact.\nPrefer this over vault_write_note + vault_delete_note, which would orphan every backlink. To only change a note's body or properties, use vault_patch_note or vault_update_properties. Protected paths cannot be moved: About Me/ and the daily notes folder (Daily Notes/ by default; not protected when it is the vault root or blank).\n\nParameters:\n- prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call.\n\nErrors:\n- \"destination exists: …\" — this tool never overwrites; pick a free new_path or delete the existing note first.\n- \"note not found: …\" — old_path does not exist; verify path with vault_list_notes.\n- \"source and destination are the same path\" — nothing to move; pass a different new_path.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is.\n- \"cannot read daily notes config from .obsidian/daily-notes.json\" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry.\n- \"path must end in …\" — add the .md extension.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move.\n- \"backlink set did not stabilize\" — notes linking to the note kept changing during the move; nothing was written; retry the move.\n- \"move aborted: …\" — nothing was written. \"could not read …\" names the note, or a note linking to it, that could not be read; fix that note, then retry. \"could not rewrite …\" says why a note could not be saved with its links rewritten, and how to fix it. \"could not write to …\", or \"could not rename to …\" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do; nothing was written.\n- \"[SqliteError]: …\" — the search index failed and nothing was written; the message says what to do.\n\nObsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).", "inputSchema": { "type": "object", "properties": { @@ -987,7 +987,7 @@ "new_path": { "type": "string", "minLength": 1, - "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder." }, "prune_empty_folders": { "default": false, @@ -1015,7 +1015,7 @@ { "name": "vault_patch_note", "title": "Patch Note", - "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nLimitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_patch_note({ path: \"Projects/plan.md\", operation: \"append\", heading: \"Open questions\", content: \"- Which region hosts the backup?\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"replace\", heading: \"Status\", content: \"On track for launch.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"insert_before\", heading: \"Phase 2\", content: \"## Phase 1\\nDone.\\n\" })\nExample: vault_patch_note({ path: \"Notes/Plan.md\", operation: \"prepend\", content: \"> [!info] Draft\\n> Not reviewed yet.\\n\" })\n\nWhen to use: Modifying part of an existing note without overwriting the entire body.\nPrefer vault_write_note for creating new notes, or full rewrites (with overwrite: true). Prefer vault_replace_in_note for in-place text changes (typos, renaming) that stay in the same location. Prefer vault_insert_at_anchor for inserting next to a specific line inside a section. Prefer vault_replace_span for replacing a run of lines inside a section. Prefer vault_delete_span for removing lines. Prefer vault_update_properties for changing frontmatter properties. Prefer vault_create_task for adding a task. Prefer vault_update_task for completing or moving a task in one write.\n\nOperations:\n- append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table; to extend a table, use vault_insert_at_anchor after its last row\n- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.\n- replace: replace section body (requires heading); to empty it, use vault_delete_span on its lines\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings (\"##\" with no text) act as boundaries but cannot be targeted — edit their content via vault_replace_in_note instead.\n\nEditing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one or use vault_replace_in_note to target by text content\n- \"operation … requires a heading target\" — pass heading, or use append or prepend to edit the file body\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it\n- \"section … has N child headings …\" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\nTable rows: send only the data row (\"| cell1 | cell2 |\"), not the header or separator — duplicating them splits the table.\n\nReturns: Confirmation message — \"Applied to → \", where target is the matched heading (e.g. \"## Active\") or \"file body\" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.", "inputSchema": { "type": "object", "properties": { @@ -1076,7 +1076,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -1121,7 +1121,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { @@ -1135,7 +1135,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -1144,13 +1144,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -1181,13 +1181,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -1218,7 +1218,7 @@ { "name": "vault_replace_in_note", "title": "Replace in Note", - "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\"). Replaces text in place; does not move content across sections.\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: non-empty new_text replaces the match exactly. A deletion (new_text=\"\") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — new_text includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \" — N is the number of matches replaced.", + "description": "Find and replace exact text in a markdown note's body. Properties are kept as written — edit them via vault_update_properties or vault_write_note's properties parameter. CRLF line breaks are saved as LF.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"- [ ] draft outline\\n\", new_text: \"\" })\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"Q3\", new_text: \"Q4\", replace_all_occurrences: true })\n\nWhen to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=\"\").\nTo delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text=\"\") — add-before-delete, so a failure duplicates the block instead of losing it.\n\nParameters:\n- old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=\"\"):\n - When text remains on the line, it is written exactly as asked.\n - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer (\"A\\n\\nB\\n\" without \"B\" → \"A\\n\").\n - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list.\n - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n- replace_all_occurrences: set true for deliberate bulk renames or term replacements.\n\nErrors:\n- \"note not found\" — path does not exist; check vault_list_notes for valid paths\n- \"path must end in …\" — add the .md extension\n- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"new_text contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: \"Replaced occurrence(s) in \".", "inputSchema": { "type": "object", "properties": { @@ -1263,7 +1263,7 @@ { "name": "vault_replace_span", "title": "Replace Span", - "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one (\"| X |\\n\") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no line contains the fragment (for end_anchor, none at or after the start line); verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — content includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", + "description": "Replace a contiguous block of whole lines in a note's body with new content, identified by short anchor substrings instead of the block's full text. Properties are kept as written, and CRLF line breaks are saved as LF.\n\nExample: vault_replace_span({ path: \"Tracker.md\", start_anchor: \"| 2024-03-02 | Acme\", content: \"| 2024-03-02 | Acme Corp | Updated |\" }) — replaces the one table row whose line contains that fragment.\nExample: vault_replace_span({ path: \"Notes/Plan.md\", start_anchor: \"> [!warning] Stale\", end_anchor: \"remove after launch\", content: \"> [!info] Current\\n> Updated for v2.\" }) — replaces the callout block with a new one.\n\nWhen to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. A span that includes a code block's opening fence can't end at a plain closing fence (```): the end anchor is ambiguous, and first_match: true would end the span at the opening fence; to replace such a block, use vault_replace_in_note.\n- content's line breaks:\n - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer.\n - Content ending in a line break (\"| X |\\n\") leaves an empty line after it, which splits a table.\n - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"start anchor not found\" / \"end anchor not found\" — no body line holds the fragment where it is searched; verify with vault_read_note\n- \"ambiguous start anchor …\" / \"ambiguous end anchor …\" — the anchor matches multiple lines; use a longer fragment or set first_match: true\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"content contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced line(s) with line(s) in \" — N counts the lines the span covered, M the lines in content as sent (its line breaks plus one).", "inputSchema": { "type": "object", "properties": { @@ -1275,17 +1275,17 @@ "start_anchor": { "type": "string", "minLength": 1, - "description": "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block." + "description": "Short, unique substring of the block's first line, a body line (case-sensitive)." }, "end_anchor": { - "description": "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", + "description": "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", "type": "string", "minLength": 1 }, "content": { "type": "string", "minLength": 1, - "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty." + "description": "Replacement content (one or more lines)." }, "first_match": { "default": false, @@ -1314,7 +1314,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -1461,7 +1461,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -1472,7 +1472,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -1502,7 +1502,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1549,14 +1549,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, @@ -1590,7 +1590,7 @@ { "name": "vault_update_memory", "title": "Update Memory", - "description": "Append a dated entry to a section of a memory file in About Me/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get \"(newest first)\" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note).\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" — memory entries are single dated bullets; collapse newlines or append multiple entries.\n- \"section must be a single line\" — section names become H2 headings; remove line breaks.\n- \"date must be a real ISO calendar date\" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. \"2026-07-02\"), not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — the value includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — use a bare file name: no folder or slash, no leading dot, and no .md.\n- \"section not created: … is nearly identical to existing section …\" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. \"2025\" vs \"2026\") count as different.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", + "description": "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.\n\nExample: vault_update_memory({ file: \"Opinions\", section: \"Code patterns (newest first)\", entry: \"Prefer immutable data structures\" })\nExample: vault_update_memory({ file: \"Routines\", section: \"Daily/weekly rhythm (newest first)\", entry: \"Gym Mon/Wed at 6am\", options: { date: \"2026-10-01\", position: \"bottom\" } }) — a backdated entry placed below the existing ones\n\nWhen to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped.\nPrefer vault_write_note for creating non-memory notes. A missing file or section is created automatically.\n\nParameters:\n- options.date only sets the bullet's date; placement follows options.position, never the date.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"entry must be a single line\" / \"section must be a single line\" — the message names the fix.\n- \"date must be a real ISO calendar date\" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp.\n- \"entry contains a control character\" / \"section contains a control character\" — remove the character the message names.\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not created: … is nearly identical to existing section …\" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section.\n- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to fix its permissions\n- \"cannot write memory file …: that path is not a file\" — a folder already has this name; choose another file name\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"cannot create memory folder …\" — a file sits where the memory folder belongs; ask the vault's owner to move it\n- \"memory file …: the note would open with a properties block …\" — saving would turn the file's opening lines into a properties block the server can't keep; the error says how to avoid it\n\nReturns: \"Added entry to About Me/.md → ##
\", plus a note to fill in the placeholder scope callout when the file is new; an exact duplicate returns \"Entry already exists in About Me/.md → ##
— nothing was written.\"", "inputSchema": { "type": "object", "properties": { @@ -1607,7 +1607,7 @@ "entry": { "type": "string", "minLength": 1, - "description": "Raw entry text — a single line; the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + "description": "Raw entry text — a single line, without the \"- **YYYY-MM-DD**: \" prefix, which the server prepends." }, "options": { "description": "Optional date and position overrides", @@ -1652,7 +1652,7 @@ { "name": "vault_update_properties", "title": "Update Properties", - "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", + "description": "Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { title: \"Todo\", status: \"active\" }, replace: true })\n\nWhen to use: Changing tags, status, type, or any property without reading/rewriting the full note body.\nPrefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. Arrays are replaced entirely, not appended to.\n\nErrors:\n- \"note not found\" — path does not exist; create the note first with vault_write_note\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks (\"[[Note]]\"), keep types consistent (mismatches cause silent query failures).\n\nReturns: \"Updated properties on \", or \"Replaced properties on \" with replace: true.", "inputSchema": { "type": "object", "properties": { @@ -1696,7 +1696,7 @@ { "name": "vault_update_task", "title": "Update Task", - "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Active\", position: 3 }) — move to the 3rd position in a lane\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line: each save by the Kanban plugin rewrites the board. The listing lags each save by a few seconds, so wait that long after a write before re-reading.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: \"delete\".\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — pass exactly one of block_id or line\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — no change params provided; pass at least one field to change\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; pass heading to move it into a section\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane holding a **Complete** line, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+\n- \"unrecognized recurrence rule ...\" — the rule text is not Tasks-plugin natural language; use a rule such as \"every week\" or \"every 2 weeks when done\"\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value. An add_subtasks change appears as \"subtasks\", its two sides checklist-item counts, and a spawn adds \"next_occurrence: (none) → line N\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", + "description": "Update a task's status, priority, description, dates, recurrence, completion action, dependencies, block_id, checklist items, heading, or position within a lane, in any combination; every field passed is written in a single edit.\n\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"done\" }) — complete a task; on a Kanban board, auto-moves to the done lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", recurrence: \"every week\" }) — make a task recurring\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\" }) — move a task to a different heading (the checkbox is unchanged)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\", scheduled: null }) — change the description, set one date, and clear another\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", status: \"in_progress\", add_subtasks: [\"Design\", \"Implement\", \"Test\"] }) — start working and add checklist stages\nExample: vault_update_task({ path: \"TASKS.md\", line: 42, assign_block_id: \"my-task\" }) — add a block_id to a task that lacks one\n\nWhen to use: Any change to an existing task. Use vault_list_tasks first to get identification fields (path + block_id or line). For creating a new task, use vault_create_task instead.\n\nParameters:\n- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line with vault_list_tasks just before an update by line. After a write, wait a few seconds for the listing to catch up.\n- At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched.\n- status side effects:\n Kanban: \"done\" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading \"Done\", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent.\n Recurring (🔁), on \"done\" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id.\n Delete (🏁 delete / [onCompletion:: delete]), on \"done\": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed.\n- recurrence: a rule ending \"when done\" bases the next occurrence on the completion day. When set together with status \"done\", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning.\n- on_completion: passed together with status, the submitted value governs the delete decision — \"keep\" while completing a \"delete\" task prevents the deletion.\n- position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion.\n\nErrors: (messages use camelCase names, e.g. assignBlockId for assign_block_id)\n- \"note not found\" — path does not exist; check the path with vault_list_notes\n- \"path must end in …\" — add the .md extension\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"exactly one of blockId or line is required\" / \"blockId and line are mutually exclusive\" — the message states the rule\n- \"blockId ... not found\" / \"blockId ... is inside a fenced code block or comment\" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id with vault_list_tasks, or target by line\n- \"blockId ... matches N task lines\" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one\n- \"no task at line N\" / \"line N is inside a fenced code block or comment\" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers with vault_list_tasks, or target by block_id\n- \"checkbox \"[c]\" is a NON_TASK status\" / \"no checkbox symbol for status ...\" — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server\n- \"at least one mutation\" — the message lists the fields that count\n- \"cannot move a sub-task to a heading\" / \"cannot reposition a sub-task\" — a heading or position on a task nested under another task (depth > 0 in vault_list_tasks); sub-tasks move with their parent, so move or reposition the parent instead\n- \"cannot reorder a task that sits above the first heading\" — position without a heading on a task before the first section heading; the message names the fix\n- \"cannot reorder within \"X\" — the heading appears N times\" / \"cannot place at position P under \"X\" — the heading appears N times\" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use \"top\" or \"bottom\" (they place under the first match), or rename one section to make it unique\n- \"heading \"X\" not found; available: ...\" — the message lists the note's headings\n- \"multiple done lanes detected\" / \"no done lane detected\" — status \"done\" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no \"Done\" heading; pass heading to pick the lane\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"assignBlockId is not allowed\" / \"description ends in a block ID\" / \"subtask ... ends in a block ID\" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text\n- \"invalid date: …\" — the message names the expected format\n- \"description cannot be empty\" / \"addSubtasks cannot contain an empty item\" / \"description must be a single line\" / \"addSubtasks items must be a single line\" — pass visible text on a single line\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — the message names the allowed characters\n- \"unrecognized recurrence rule ...\" — the message gives example rules\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like \"🔁 every week\", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves.\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }.\n- line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal.\n- description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading.\n- subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update.\n- next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has.\n- changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value; an add_subtasks change is listed under \"subtasks\", a spawn under \"next_occurrence\", and a 🏁 delete under \"on_completion\".\n- advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left).\n- on_completion_applied is \"delete\", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.", "inputSchema": { "type": "object", "properties": { @@ -1772,7 +1772,7 @@ ] }, "description": { - "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "description": "New task description text; metadata fields and block_id are preserved.", "type": "string", "minLength": 1 }, @@ -1853,7 +1853,7 @@ ] }, "add_subtasks": { - "description": "Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "description": "Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete). For full sub-tasks with metadata, use vault_create_task with parent_block_id.", "minItems": 1, "type": "array", "items": { @@ -1879,7 +1879,7 @@ "minLength": 1 }, "position": { - "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", + "description": "Where within the target heading the task lands. \"top\" or \"bottom\" for the extremes; an integer (1-based) for an exact position among the lane's top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike \"bottom\", which appends after any non-task text at the end of the section). Defaults to \"top\" on heading and done-lane moves.", "anyOf": [ { "type": "string", @@ -1923,7 +1923,7 @@ { "name": "vault_write_note", "title": "Write Note", - "description": "Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — body includes a non-printable control byte; remove it before writing\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block the server cannot keep …\" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", + "description": "Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null.\n\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"# Notes\\n\\nProject notes here.\", properties: { tags: [\"project\"], type: \"project\" } })\nExample: vault_write_note({ path: \"Projects/notes.md\", body: \"Updated content.\", overwrite: true })\n\nWhen to use: Creating a new note, or replacing a whole note's body.\nPrefer vault_patch_note or vault_replace_in_note to change part of a note.\nPrefer vault_update_properties for property-only edits (no body round-trip).\nPrefer vault_update_memory for appending dated entries to About Me/ memory files.\n\nErrors:\n- \"note already exists\" — set overwrite: true to replace it\n- \"path must end in …\" — add the .md extension\n- \"cannot write note …: that path is not a file\" — a folder already has this name; choose another path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n- \"body contains a control character\" — remove the character the message names\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"properties block …\" — with overwrite: true, the note's properties block can't be read, or a rewrite would lose it; the error says how to repair it\n- \"the note would open with a properties block …\" — the edit would leave --- lines at the top of a note with no properties, around text that can't be kept as properties; the error says how to avoid it\n\nObsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values (\"[[Note]]\"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures).\n\nReturns: \"Wrote \".", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json index 60b085d26..25cb9f334 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -45,7 +45,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -100,7 +100,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -110,12 +110,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -136,7 +136,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -165,7 +165,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -207,7 +207,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. Always call this first to get valid file and section names.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -226,7 +226,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -257,7 +257,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -283,7 +283,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -324,7 +324,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -343,13 +343,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -573,7 +573,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -587,7 +587,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -611,7 +611,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", + "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", "inputSchema": { "type": "object", "properties": { @@ -621,7 +621,7 @@ "description": "Topic to recall — use specific keywords." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -652,7 +652,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -697,7 +697,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -711,7 +711,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -720,13 +720,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -757,13 +757,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -794,7 +794,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -941,7 +941,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -952,7 +952,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -982,7 +982,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1029,14 +1029,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json index 7627a6527..396b9265c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -46,7 +46,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -75,7 +75,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -101,7 +101,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -111,12 +111,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -137,7 +137,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -166,7 +166,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. Always call this first to get valid file and section names.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -185,7 +185,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -216,7 +216,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -242,7 +242,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -283,7 +283,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -302,13 +302,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -532,7 +532,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -546,7 +546,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -570,7 +570,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", + "description": "Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. \"pacing\", then \"recovery\", then \"sustainable hours\"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. In keyword-only mode search_mode is always \"fts\" and reranked always false.", "inputSchema": { "type": "object", "properties": { @@ -580,7 +580,7 @@ "description": "Topic to recall — use specific keywords." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -611,7 +611,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -625,7 +625,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -634,13 +634,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -671,13 +671,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -708,7 +708,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -855,7 +855,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -866,7 +866,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -896,7 +896,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -943,14 +943,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json index fca6d9ca8..4073ab461 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -45,7 +45,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -100,7 +100,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -110,12 +110,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -136,7 +136,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -165,7 +165,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. Always call this first to get valid file and section names.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -184,7 +184,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -215,7 +215,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -241,7 +241,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -282,7 +282,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -301,13 +301,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -531,7 +531,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -545,7 +545,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -569,7 +569,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -579,7 +579,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -610,7 +610,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -624,7 +624,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -633,13 +633,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -670,13 +670,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -707,7 +707,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -854,7 +854,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -865,7 +865,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -895,7 +895,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -942,14 +942,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json index fd2afeb10..0a98d681f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -46,7 +46,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -75,7 +75,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -101,7 +101,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -130,7 +130,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -172,7 +172,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -203,7 +203,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -270,7 +270,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -289,13 +289,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -519,7 +519,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -533,7 +533,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -557,7 +557,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -602,7 +602,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -616,7 +616,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -625,13 +625,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -662,13 +662,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -699,7 +699,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes and file content (canvas, PDF, and text files), ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -846,7 +846,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -857,7 +857,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -887,7 +887,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -934,14 +934,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json index 5946c467b..0ea15002b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -11,7 +11,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -47,7 +47,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -76,7 +76,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -102,7 +102,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -131,7 +131,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -162,7 +162,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -188,7 +188,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -229,7 +229,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -248,13 +248,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -478,7 +478,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -492,7 +492,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -516,7 +516,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -530,7 +530,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -539,13 +539,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -576,13 +576,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -613,7 +613,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns { results: [], total: 0 }\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Full-text search across vault notes, ranked by relevance. Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\nExample: vault_search({ query: \"deployment\", filters: { properties: { status: \"active\" } } })\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before matching; a query left with no terms returns no results\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -760,7 +760,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -771,7 +771,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -801,7 +801,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -848,14 +848,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json index edfc4d96b..1a7b4f11b 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -10,7 +10,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -46,7 +46,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -75,7 +75,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -101,7 +101,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF). When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links).\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search or vault_list_notes.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -130,7 +130,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -161,7 +161,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -187,7 +187,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -247,13 +247,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -477,7 +477,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -491,7 +491,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -515,7 +515,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -529,7 +529,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -538,13 +538,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -575,13 +575,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -612,7 +612,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes, ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is always \"note\". The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -759,7 +759,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -770,7 +770,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -800,7 +800,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -847,14 +847,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json index 49dd80f88..b44f0e960 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -9,7 +9,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -45,7 +45,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -74,7 +74,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -100,7 +100,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -129,7 +129,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -171,7 +171,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -202,7 +202,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -228,7 +228,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -269,7 +269,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -288,13 +288,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -518,7 +518,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -532,7 +532,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -556,7 +556,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -601,7 +601,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -615,7 +615,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -624,13 +624,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -661,13 +661,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -698,7 +698,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -845,7 +845,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"Projects/Archive\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -856,7 +856,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -886,7 +886,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -933,14 +933,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json index 1c95ea3e5..456095314 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -8,7 +8,7 @@ { "name": "vault_find_orphans", "title": "Find Orphans", - "description": "Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.\n- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass a shorter exclude_folders list, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan.\n\nExample: vault_find_orphans({})\nExample: vault_find_orphans({ exclude_folders: [\"Templates\", \"Archive\"], limit: 20 })\n\nWhen to use: Vault maintenance — surfacing notes to integrate into the graph.\nPrefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.\n\nParameters:\n- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included (\"Projects\" also excludes \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults' daily notes folder is the one vault_get_daily_note's path starts with.\n- limit applies after exclusions and sorting; nothing in the response signals truncation.\n\nBehavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n- \"too many excluded folders\" — pass at most 994 exclude_folders entries, then retry.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -44,7 +44,7 @@ { "name": "vault_get_backlinks", "title": "Get Backlinks", - "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and canvases that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas cards that embed the file. Heading anchors ([[note#heading]]) and aliases ([[note|alias]]) resolve as backlinks to the base note. Links inside code blocks are ignored; a note linking to itself appears in its own backlinks.\n\nExample: vault_get_backlinks({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_backlinks({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.\nFor outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans.\n\nParameters:\n- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -73,7 +73,7 @@ { "name": "vault_get_daily_note", "title": "Get Daily Note", - "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date: past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).", + "description": "Read a daily note by date, using the vault's configured Daily Notes folder and filename date format. Each setting comes from the DAILY_NOTES_FOLDER / DAILY_NOTES_FORMAT env vars, falling back to the vault's .obsidian/daily-notes.json, then to \"Daily Notes\" and YYYY-MM-DD.\n\nExample: vault_get_daily_note({ date: \"2026-05-13\" })\nExample: vault_get_daily_note({}) — returns today's daily note\n\nWhen to use: When you need today's or a specific date's daily note. For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.\n\nParameters:\n- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format\n- \"… is not a calendar date\" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"daily note format contains unsupported token(s): ...\" — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix\n- \"path traversal blocked\" / \"hidden path blocked\" / \"absolute path blocked\" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).", "inputSchema": { "type": "object", "properties": { @@ -99,7 +99,7 @@ { "name": "vault_get_memory", "title": "Get Memory", - "description": "Read semantic memory from About Me/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet.\n\nWith file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — section or on_or_after was passed without file; add file\n- \"memory file not found\" — file does not exist in About Me/; call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — pass the file's bare name: no folder or slash, no leading dot, and no .md\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", + "description": "Read semantic memory from About Me/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large.\n\nWith file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution.\n\nExample: vault_get_memory({ file: \"Principles\" }) — the whole file\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\nExample: vault_get_memory({ file: \"Opinions\", section: \"Code patterns\", on_or_after: \"2026-09-01\" })\nExample: vault_get_memory({ file: \"Opinions\", on_or_after: \"2026-09-01\" }) — every section's entries since the date\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes. To find entries by topic across every file, use vault_memory_recall.\n\nErrors:\n- \"section requires a file\" / \"on_or_after requires a file\" — add file\n- \"memory file not found\" — call vault_list_memory_files to discover valid names\n- \"memory file must not start with a dot\" / \"memory file must be a bare name without path separators\" / \"memory file must be named without .md\" — each message names the part of the name to drop\n- \"section not found: …\" — no H2 heading matches; the message lists the file's sections\n- \"date must be a real ISO calendar date\" — on_or_after must be a valid YYYY-MM-DD date\n- \"cannot read memory file …\" / \"cannot list memory folder …\" — a memory file, or the About Me/ folder itself on a call without file, can't be read: check file against vault_list_memory_files and shorten an over-long name; otherwise ask the vault's owner to check the folder\n\nReturns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.", "inputSchema": { "type": "object", "properties": { @@ -109,12 +109,12 @@ "minLength": 1 }, "section": { - "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Omit to read the whole file.", + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix.", "type": "string", "minLength": 1 }, "on_or_after": { - "description": "Inclusive date filter (YYYY-MM-DD). Requires a file.", + "description": "Inclusive date filter (YYYY-MM-DD).", "type": "string", "minLength": 1 } @@ -135,7 +135,7 @@ { "name": "vault_get_outgoing_links", "title": "Get Outgoing Links", - "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", + "description": "Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded.\n\nExample: vault_get_outgoing_links({ path: \"Projects/vault-cortex.md\" })\nExample: vault_get_outgoing_links({ path: \"Diagrams/architecture.canvas\" })\n\nWhen to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.\nFor incoming links (what links TO a note), use vault_get_backlinks.\n\nParameters:\n- path: a note or canvas created or moved in the last few seconds is not found yet.\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind \"note\" is a markdown note readable via vault_read_note; kind \"file\" is a non-markdown file (.canvas, image, PDF) readable via vault_read_file. When exists is false the link is broken, and kind is \"note\" even for a path like \"diagram.png\". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read.\n\nErrors:\n- \"path must end in …\" — add the .md or .canvas extension\n- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, find valid paths with vault_search, vault_list_notes, or vault_list_files.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do", "inputSchema": { "type": "object", "properties": { @@ -164,7 +164,7 @@ { "name": "vault_list_files", "title": "List Files", - "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.", + "description": "List non-markdown files in the vault or a folder — images, canvases, PDFs, data files — with per-file byte sizes and per-extension counts.\n\nExample: vault_list_files({}) — non-markdown files across the whole vault\nExample: vault_list_files({ folder: \"attachments\" })\nExample: vault_list_files({ extensions: [\".png\", \".jpg\"], limit: 20 })\n\nWhen to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nBehavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — the folder is absolute, escapes the vault, names the vault root itself (e.g. \".\"), or is hidden like \".obsidian\"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is \"(none)\" for a file without one), extension_counts (counts keyed by extension, e.g. { \".png\": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", "inputSchema": { "type": "object", "properties": { @@ -206,7 +206,7 @@ { "name": "vault_list_memory_files", "title": "List Memory Files", - "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line (\"- **YYYY-MM-DD**: text\"). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns [{ file: \"Principles\", title: \"Principles\", bytes: 2048, entry_policy: \"append-only\", leading_callout: null, headings: [{ level: 2, text: \"Decision heuristics (newest first)\", entry_count: 12 }] }, ...]\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. Always call this first to get valid file and section names.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file sits where the memory folder belongs, or its permissions need the vault's owner\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", + "description": "Discovery tool — lists About Me/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries.\n\nExample: vault_list_memory_files()\n\nWhen to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. To find entries by topic instead, use vault_memory_recall.\n\nBehavior: Lists every .md file directly inside About Me/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped.\n\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n- \"cannot list memory folder …\" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it\n- \"cannot read memory file …\" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a \"Scope of this file\" block describing what belongs in the file), or null; entry_policy is \"append-only\" (the default: by convention, entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter). The server does not enforce either policy.", "inputSchema": { "type": "object", "properties": {}, @@ -225,7 +225,7 @@ { "name": "vault_list_notes", "title": "List Notes", - "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", + "description": "List .md file paths in the vault, optionally filtered by folder and/or glob pattern. Returns paths only — not content or metadata.\n\nExample: vault_list_notes({ folder: \"Projects\" })\nExample: vault_list_notes({ glob: \"**/*session-log*.md\" })\nExample: vault_list_notes({ folder: \"Projects\", glob: \"*.md\" }) — the folder's top-level notes only\n\nWhen to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.\nPrefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results.\n\nParameters:\n- folder names a whole folder; without a glob, notes in its subfolders are listed too: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing.\n- glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder \"Projects\", \"**/*.md\" lists every note under it.\n\nBehavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault.\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message names the folder it could not read, and what to do; until then, pass a folder outside it.\n\nReturns: JSON array of every matching vault-relative path, with no cap (e.g. [\"Notes/idea.md\", \"Projects/plan.md\"]).", "inputSchema": { "type": "object", "properties": { @@ -256,7 +256,7 @@ { "name": "vault_list_property_keys", "title": "List Property Keys", - "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", + "description": "Discover all property keys in the vault with note counts and sample values, without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\nExample: vault_list_property_keys({ folder: \"Projects\" }) — the keys used under Projects/ only\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to scan the entire vault.\n\nBehavior:\n- Only frontmatter properties count; inline Dataview fields (key:: value) are not listed.\n- count is the number of notes that have the key, including notes where its value is empty (null).\n- sample_values are the key's 3 most frequent displayed strings, counting each array element separately; a checkbox appears as \"1\" or \"0\", and null values are skipped.\n\nErrors:\n- An empty vault or folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending, then by key.", "inputSchema": { "type": "object", "properties": { @@ -282,7 +282,7 @@ { "name": "vault_list_property_values", "title": "List Property Values", - "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like \"title\" or \"created\"; raise limit to check.\n\nBehavior:\n- Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", + "description": "List distinct values for a specific property key with how often each occurs.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"done\", count: 211 }, { value: \"active\", count: 47 }, ...]\nExample: vault_list_property_values({ key: \"tags\", limit: 10 }) — the 10 most-used tags\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Call vault_list_property_keys first to discover valid key names. vault_search_by_property matches stored numbers numerically and text exactly; value \"1\" matches the number 1, the text \"1\", and a checked checkbox.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys.\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case.\n- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation.\n\nBehavior:\n- Array elements (tags: [\"a\", \"b\"]) are counted individually, so the sum of counts may exceed the note count.\n- Values with the same displayed string share one row with combined occurrence counts: number 1 and text \"1\" count together, while text \"1.0\" stays separate. Grouping happens before limit.\n- Checkbox true and false come back as \"1\" and \"0\", counted with the numbers 1 and 0.\n- null values are skipped.\n\nErrors:\n- An unknown key or empty folder returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { value, count } sorted by count descending, then by value in binary text order (\"10\" before \"2\").", "inputSchema": { "type": "object", "properties": { @@ -323,7 +323,7 @@ { "name": "vault_list_tags", "title": "List Tags", - "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching (\"project\" matches \"project/*\").\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", + "description": "List all tags in the vault with note counts, ordered by count descending, then by tag name. Only frontmatter tags Obsidian accepts are counted, not inline #tags. Spellings that differ only in letter case are one tag, shown as the spelling written most often. Each hierarchical tag appears as one full entry, not split into segments. Count is unique notes, not occurrences.\n\nExample: vault_list_tags() returns [{ tag: \"session-log\", count: 42 }, { tag: \"project/vault-cortex\", count: 8 }, ...]\n\nWhen to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.\nPrefer vault_search_by_tag once you know which tag to query. To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit.\n\nErrors:\n- A vault with no tagged notes returns an empty array, not an error.\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of { tag, count }; tag omits the \"#\" prefix.", "inputSchema": { "type": "object", "properties": {}, @@ -342,13 +342,13 @@ { "name": "vault_list_tasks", "title": "List Tasks", - "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\nExample: vault_list_tasks({ top_level_only: true, path: \"TASKS.md\" }) — board cards only, excluding checklist sub-items\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in .md\" — add the \".md\" extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "description": "List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded.\n\nExample: vault_list_tasks({ due: { before: \"2026-07-04\" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the \"what's overdue?\" call\nExample: vault_list_tasks({ path: \"Code Projects/vault-cortex/TASKS.md\", heading: [\"Active\", \"Up Next\", \"Waiting On\"], sort_by: \"position\" }) — actionable Kanban lanes in board order\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories)\nExample: vault_list_tasks({ status: \"done\", done: { after: \"2026-06-26\" } }) — what got completed this week\n\nWhen to use: Reading task status and order on one board (path + sort_by: \"position\"; add status: \"all\" to include done and cancelled cards). Also any vault-wide task triage question — \"what's overdue?\", \"what's open per project?\", \"what did I finish this week?\" — in one call instead of per-board reads.\nPrefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. Prefer vault_search for full-text queries over note content.\n\nBehavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state.\n\nParameters:\n- status: \"not_done\" and \"all\" expand in arrays — [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date.\n- folder: a whole folder, subfolders included (\"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"), ignoring ASCII letter case.\n- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, and tasks with no priority sort between medium and low.\n\nErrors:\n- \"invalid due.before date: …\" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"path must end in …\" — add the .md extension\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name\n- No matches returns { total: 0, tasks: [] }, not an error\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as \"x\" or \"/\"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", "inputSchema": { "type": "object", "properties": { "status": { "default": "not_done", - "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). \"all\" includes every status.", + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress).", "anyOf": [ { "type": "string", @@ -572,7 +572,7 @@ }, "sort_by": { "default": "due", - "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). \"position\" sorts by file path then line number.", "type": "string", "enum": [ "due", @@ -586,7 +586,7 @@ ] }, "sort_direction": { - "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.", "type": "string", "enum": [ "asc", @@ -610,7 +610,7 @@ { "name": "vault_memory_recall", "title": "Memory Recall", - "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nErrors:\n- No matching entries returns { entries: [], total: 0 }, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is \"hybrid\" when vector matching contributed and \"fts\" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.", + "description": "Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries (\"testing philosophy\", \"sustainable pacing\") rank best. A meta-framed query (\"opinions on testing\") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.\n\nExample: vault_memory_recall({ query: \"working hours and pacing\" })\nExample: vault_memory_recall({ query: \"testing philosophy\", file: \"Opinions\" })\n\nWhen to use: Answering \"what does my memory say about X?\" or \"how has my view on Y evolved?\" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer.\n\nBehavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet.\n\nErrors:\n- No matching entries returns entries: [] and total: 0, not an error\n- An unknown file returns empty results — call vault_list_memory_files to discover valid names\n- \"[SqliteError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_get_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is \"hybrid\" when semantic matching contributed and \"fts\" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.", "inputSchema": { "type": "object", "properties": { @@ -620,7 +620,7 @@ "description": "Topic to recall, in natural language." }, "file": { - "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", + "description": "Restrict to one memory file, name without .md (e.g. \"Opinions\"), in its exact letter case. Omit for cross-file recall — the default and usual choice.", "type": "string", "minLength": 1 }, @@ -651,7 +651,7 @@ { "name": "vault_read_file", "title": "Read File", - "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, 49152 bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline.\n- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable)\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file\n- \"PDF page rendering failed\" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted\" — the image could not be compressed under the image output budget; ask for a smaller export\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read (for example by Sync); retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", + "description": "Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note.\n\nExample: vault_read_file({ path: \"attachments/diagram.png\" }) — the image itself, shrunk when too large\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"exports/data.json\" }) — the file content as text\nExample: vault_read_file({ path: \"exports/big.csv\", limit: 500 }) — the first 500 lines\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\n\nWhen to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links. Browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total.\n\nWhat each file type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (49152 bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed.\n- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels.\n- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer.\n- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text.\n\nraw: true switches a canvas or PDF to its other form:\n- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline.\n- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first 5 pages are rendered; later pages are readable only as text, without raw.\n\nLine paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact.\n\nErrors:\n- \"not a file: … is a markdown note\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"file too large\" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise\n- \"text output too large\" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows\n- \"start line past the end\" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line\n- \"line range is not available\" (start_line or limit on an image, or on a PDF read with raw: true) / \"raw source is not available for images\" (raw on an image) — drop that input\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy\n- \"invalid .canvas JSON\" — the canvas file is empty or not valid JSON; set raw: true to read its source as text\n- \"PDF has no extractable text\" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead\n- \"PDF is password-protected\" — this tool cannot supply a password; read an unprotected copy instead\n- \"PDF is damaged or not a PDF\" / \"could not decode image\" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file\n- \"PDF page rendering failed\" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF\n- \"image cannot be fitted into …\" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n- \"file changed while reading\" — the file was written while it was read; retry the read\n- \"PDF reader could not start\" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server\n- \"unsupported file type …\" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size\n\nReturns: for images, an image content block plus a one-line metadata text block; for PDFs with raw: true, a metadata text block followed by alternating image and text blocks (one pair per page); for every other supported type, a single text content block — preceded by a window-metadata text block when start_line or limit was given.", "inputSchema": { "type": "object", "properties": { @@ -696,7 +696,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 })\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — the message lists the available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive — set at most one\"\n- \"heading_level requires a heading\" — pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"… properties_only mode\" — start_line and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count; resend a start_line within it\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — a full read, outline and heading still work; the error says how to repair it\n- \"ENAMETOOLONG: …\", \"EACCES: …\" or another filesystem error code — the message says what to do\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note.", "inputSchema": { "type": "object", "properties": { @@ -710,7 +710,7 @@ "type": "boolean" }, "outline": { - "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "description": "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", "type": "boolean" }, "heading": { @@ -719,13 +719,13 @@ "minLength": 1 }, "heading_level": { - "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", "type": "integer", "minimum": 1, "maximum": 6 }, "start_line": { - "description": "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "description": "First line to return, 1-based (default 1).", "type": "integer", "minimum": 1, "maximum": 9007199254740991 @@ -756,13 +756,13 @@ { "name": "vault_recent_notes", "title": "Recent Notes", - "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", + "description": "List recently modified or created notes, sorted by timestamp — a time-ordered window into the vault, not a date-range filter.\n\nExample: vault_recent_notes({ sort_by: \"modified\", limit: 10 })\nExample: vault_recent_notes({ sort_by: \"created\", limit: 5 })\n\nWhen to use: Catching up on vault changes, finding recent work, or orienting after a break.\nPrefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" uses filesystem mtime, which every note has. \"created\" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use \"modified\" to see them.\n- sort_by \"modified\" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits.\n\nBehavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet.\n\nErrors:\n- An empty vault returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), newest first. The created field is null when the property is missing or not an ISO date; bytes is on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { "sort_by": { "default": "modified", - "description": "Timestamp to sort by, newest first (default \"modified\")", + "description": "Timestamp to sort by (default \"modified\")", "type": "string", "enum": [ "created", @@ -793,7 +793,7 @@ { "name": "vault_search", "title": "Search Notes", - "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nErrors:\n- No matches returns { results: [], total: 0 }, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form\n- \"… is not a calendar date\" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "description": "Hybrid search across vault notes and file content (canvas, PDF, and text files), ranked by combined keyword and semantic relevance. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Unquoted terms use implicit AND with stemming (\"running\" also matches \"run\"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.\n\nFilters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:\n- folder: a whole folder, subfolders included — \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\"; ignores ASCII letter case\n- tags: every listed frontmatter tag must match, in any letter case; \"project\" also matches \"project/a\"\n- properties: keys and text values match exactly, letter case included; values compare by exact type — pass a number as a number, not \"4\". Exception: true and false compare as 1 and 0, so true or 1 matches a checked checkbox and the number 1. A list property matches when any element equals the value.\n- created / modified: bounds compare whole calendar days in the server's time zone (returned created and modified values show its UTC offset), so \"on\" matches anywhere within the day. Notes without a parseable created property never match a created filter\n\nExample: vault_search({ query: \"how the server watches for file changes\" })\nExample: vault_search({ query: \"kubernetes networking\", filters: { tags: [\"reference\"] } })\nExample: vault_search({ query: \"meeting notes\", filters: { type: \"meeting\", folder: \"Work\" } })\nExample: vault_search({ query: \"decision\", filters: { modified: { after: \"2026-06-30\" } } }) — matching notes touched in July or later\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata.\nPrefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing.\n\nBehavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet.\n\nErrors:\n- No matches returns an empty results array with total 0, not an error\n- Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before keyword matching; a query left with no terms is matched by meaning only, or finds nothing when only keyword matching ran (search_mode \"fts\")\n- \"invalid created.on date: …\" or \"… is not a calendar date\" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form\n- \"tag must not be empty …\" — a tags entry is only \"#\"; pass the tag name\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes), total (results returned, not all matches), search_mode (\"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching ran), and reranked (true when reranking refined the ordering). A higher score is more relevant. The kind field is \"note\" for markdown notes or \"file\" for non-markdown content; file results also carry extension (e.g. \".pdf\"). The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", "inputSchema": { "type": "object", "properties": { @@ -940,7 +940,7 @@ { "name": "vault_search_by_folder", "title": "Search by Folder", - "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", + "description": "Browse notes in a folder with full metadata.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\nExample: vault_search_by_folder({ folder: \"About Me\", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent\n\nWhen to use: Exploring a folder's contents with full context for vault orientation.\nPrefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault.\n\nParameters:\n- folder names a whole folder, not a text prefix: \"Projects\" matches notes under \"Projects/\" but not \"ProjectsOld/\". Matching ignores ASCII letter case, and a trailing slash is ignored.\n- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).", "inputSchema": { "type": "object", "properties": { @@ -951,7 +951,7 @@ }, "recursive": { "default": true, - "description": "Include subfolders (default: true); false lists only the folder's top level", + "description": "Include subfolders (default: true)", "type": "boolean" }, "limit": { @@ -981,7 +981,7 @@ { "name": "vault_search_by_property", "title": "Search by Property", - "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary, \"1e999\") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and \"1e-999\" matches 0.\n- Pass a checkbox as \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- An array element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.", + "description": "Find notes where a frontmatter property matches a value.\n\nExample: vault_search_by_property({ key: \"status\", value: \"in-progress\" })\nExample: vault_search_by_property({ key: \"type\", value: \"session-log\", folder: \"Code Projects\" })\n\nWhen to use: Finding notes by metadata when you don't have a text query.\nPrefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes.\n\nParameters:\n- key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing.\n- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: \"04\" and \"4.0\" match number 4 and their own literal text, but not text \"4\". Anything else (\"4abc\", \" 4\", 0b binary) matches only as text.\n- value for a checkbox: pass \"1\" (checked) or \"0\" (unchecked); a checkbox matches only as text, so \"1.0\" and \"true\" do not match.\n- value against a list property: an element must equal value in full: \"blog\" matches tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder names a whole folder and includes its subfolders: \"Projects\" covers \"Projects/Archive\" but not \"ProjectsOld/\". Matching ignores ASCII letter case; omit folder to search the entire vault.\n- limit keeps the most recently modified matches; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value.\n\nErrors:\n- An unknown key or unmatched value returns an empty array, not an error.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits.\n- folder is the top-level folder (\"\" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date.\n- leading_callout appears only when the note has a leading callout.\n- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.", "inputSchema": { "type": "object", "properties": { @@ -1028,14 +1028,14 @@ { "name": "vault_search_by_tag", "title": "Search by Tag", - "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", + "description": "Find notes by frontmatter tag, ignoring letter case; inline #tags in note bodies are not searched. Unless exact is true, a tag also matches every tag nested under it at any depth: \"project\" matches \"project/vault-cortex\" and \"project/a/b\". A nested tag continues with \"/\", so \"project\" never matches \"projects\" or \"my-project\".\n\nExample: vault_search_by_tag({ tag: \"project\" })\nExample: vault_search_by_tag({ tag: \"project\", exact: true, limit: 50 }) — notes tagged \"project\" itself, up to 50\n\nWhen to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query. Use vault_list_tags first to discover available tags.\nPrefer vault_search when you need text-based relevance ranking.\n\nParameters:\n- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation.\n\nBehavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error.\n- \"project/\" matches neither \"project\" nor \"project/a\"; drop the \"/\".\n- \"tag must not be empty …\" — tag is \"#\" alone; pass the tag name.\n- \"[SqliteError]: …\" / \"[IndexIntegrityError]: …\" — the search index failed, not the call; the message says what to do\n\nReturns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; \"\" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).", "inputSchema": { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, - "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\"). Nested tags use \"/\" (e.g. \"project/vault-cortex\")." + "description": "Tag, \"#\" optional (e.g. \"project\", \"session-log\", \"project/vault-cortex\")" }, "exact": { "default": false, diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index efbd49469..43bc2d4e7 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -213,16 +213,27 @@ describe("registerTools", () => { }) it("vault_replace_in_note description clarifies in-place scope", () => { - const [, config] = requireCall(TOOL_NAMES.VAULT_REPLACE_IN_NOTE) - expect(config.description).toContain("in place") - expect(config.description).toContain("vault_read_note") + // A single location in the opener, and relocation across headings routed + // to vault_patch_note + const whenToUse = extractDescriptionSection({ + registeredCalls: calls, + toolName: TOOL_NAMES.VAULT_REPLACE_IN_NOTE, + startMarker: "When to use:", + endMarker: "\n\nParameters:", + }) + expect(whenToUse).toBe( + [ + 'When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text="").', + 'To delete a large multi-line block, prefer vault_delete_span (short anchors instead of full old_text). To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text="") — add-before-delete, so a failure duplicates the block instead of losing it.', + ].join("\n"), + ) }) it("vault_read_note documents the outline response on the outline parameter", () => { // The only guard against this drifting from the actual response shape. const [, config] = requireCall(TOOL_NAMES.VAULT_READ_NOTE) expect(config.inputSchema?.shape.outline?.description).toBe( - "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", ) }) @@ -246,25 +257,34 @@ describe("registerTools", () => { }) it("vault_patch_note description routes a section-above-first-heading insert to insert_before", () => { - const [, config] = requireCall(TOOL_NAMES.VAULT_PATCH_NOTE) - expect(config.description).toContain( - "To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend.", + const prependEntry = findDescriptionLine({ + registeredCalls: calls, + toolName: TOOL_NAMES.VAULT_PATCH_NOTE, + linePrefix: "- prepend:", + }) + expect(prependEntry).toBe( + "- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section.", ) }) it("vault_patch_note description does not list the displacement advisory as an error", () => { // Agents treat Errors: as failure modes — a successful write described // there would prompt a needless retry or abort. - const [, config] = requireCall(TOOL_NAMES.VAULT_PATCH_NOTE) - const description = requireDescription(config, TOOL_NAMES.VAULT_PATCH_NOTE) const errorsSection = extractDescriptionSection({ registeredCalls: calls, toolName: TOOL_NAMES.VAULT_PATCH_NOTE, startMarker: "Errors:", endMarker: "Obsidian syntax:", }) - expect(errorsSection).not.toContain("becomes the new section's body") - expect(description).toContain("becomes the new section's body") + const returnsLine = findDescriptionLine({ + registeredCalls: calls, + toolName: TOOL_NAMES.VAULT_PATCH_NOTE, + linePrefix: "Returns:", + }) + expect(errorsSection).not.toContain("nest") + expect(returnsLine).toBe( + 'Returns: Confirmation message — "Applied to → ", where target is the matched heading (e.g. "## Active") or "file body" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content\'s size and any call that would have avoided it.', + ) }) it("vault_update_task description does not list the no-next-occurrence advisory as an error", () => { @@ -290,11 +310,14 @@ describe("registerTools", () => { }) it("vault_update_memory description documents the duplicate no-op contract", () => { - const [, config] = requireCall(TOOL_NAMES.VAULT_UPDATE_MEMORY) - // Assert the full contract fragment — a bare "idempotent" check would - // also pass on a reworded "not idempotent" description. - expect(config.description).toContain( - "Idempotent — an exact duplicate (same date + text in the same section) is a no-op", + // The whole opener, so a reworded "is not a no-op" can't pass + const opener = findDescriptionLine({ + registeredCalls: calls, + toolName: TOOL_NAMES.VAULT_UPDATE_MEMORY, + linePrefix: "Append a dated entry", + }) + expect(opener).toBe( + "Append a dated entry to a section of a memory file in About Me/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare `entry-policy: living` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries with vault_delete_memory is expected maintenance.", ) }) @@ -316,7 +339,7 @@ describe("registerTools", () => { linePrefix: '- "ambiguous: N entries match …"', }) expect(ambiguousEntry).toBe( - '- "ambiguous: N entries match …" — more than one identical bullet exists in the section. Remove the extra copy with vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or a manual edit, then retry.', + '- "ambiguous: N entries match …" — remove the extra copy with vault_delete_span (first_match: true) or a manual edit, then retry.', ) }) @@ -888,9 +911,9 @@ describe("vault_update_memory handler", () => { /** The registered handler over a real temp vault holding one memory file, * so it exercises the actual memory store — the global harness registers * against a nonexistent path. */ - const setupUpdateMemoryHandler = async (): Promise< - (args: Record) => Promise - > => { + const setupUpdateMemoryHandler = async ( + configEnv: Record = {}, + ): Promise<(args: Record) => Promise> => { const tempVault = await mkdtemp(join(tmpdir(), "tool-definitions-memory-")) onTestFinished(() => rm(tempVault, { recursive: true, force: true })) await mkdir(join(tempVault, "About Me"), { recursive: true }) @@ -905,7 +928,7 @@ describe("vault_update_memory handler", () => { vaultPath: tempVault, search: {} as SearchIndex, logger, - config: loadConfig({}), + config: loadConfig(configEnv), }) const registeredCalls = server.registerTool.mock.calls as RegisterToolCall[] const updateMemoryCall = registeredCalls.find( @@ -963,6 +986,21 @@ describe("vault_update_memory handler", () => { }) }) + it("names no editing tool in the new-file nudge when vault_replace_in_note is disabled", async () => { + const updateMemory = await setupUpdateMemoryHandler({ DISABLED_TOOLS: "vault_replace_in_note" }) + + expect( + await updateMemory({ file: "Health", section: "Sleep", entry: "Aims for 8 hours" }), + ).toEqual({ + content: [ + { + type: "text", + text: 'Added entry to About Me/Health.md → ## Sleep. Created a new memory file with a placeholder scope callout — replace its "(describe what belongs in this file — and what doesn\'t)" placeholder with what the file contains, so other agents know what it is for.', + }, + ], + }) + }) + it("confirms an entry that starts a new section of an existing file without the new-file nudge", async () => { const updateMemory = await setupUpdateMemoryHandler() @@ -1184,15 +1222,15 @@ describe("vault_search description reflects FILE_TOOLS_ENABLED", () => { filtersLeadIn: "Filters — all conditions AND-combine with each other and the text query; any filter but folder limits results to notes:", resultFields: - "results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes", + "results array (path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes", kindSentence: - 'The kind field is "note" for markdown notes or "file" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".pdf").', + 'The kind field is "note" for markdown notes or "file" for non-markdown content; file results also carry extension (e.g. ".pdf").', } const NOTE_ONLY_WORDING = { searchedScope: "search across vault notes", filtersLeadIn: "Filters — all conditions AND-combine with each other and the text query:", resultFields: - "results array (path, title, snippet, score, tags, folder, type, kind, created, modified, bytes", + "results array (path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes", kindSentence: 'The kind field is always "note".', } @@ -1260,17 +1298,17 @@ describe("vault_delete_note description reflects OBSIDIAN_SYNC", () => { '- "ENAMETOOLONG: …", "EACCES: …" or another filesystem error code — the message says what to do; the note stays put', ] const DAILY_NOTES_CONFIG_ERROR_ENTRY = - '- "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry' + '- "cannot read daily notes config from .obsidian/daily-notes.json" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry' it("lists the trash-move and trash-setting errors when the server does not sync", () => { expect(deleteNoteErrors({})).toBe( [ "Errors:", ...PATH_AND_LOOKUP_ERROR_ENTRIES, - '- "cannot move to trash … — 100 collisions in .trash/" — .trash/ holds this name and 100 numbered copies ("Plan 1.md" … "Plan 100.md"); ask the vault owner to clear old copies, then retry', + '- "cannot move to trash … — 100 collisions in .trash/" — .trash/ holds this name and 100 numbered copies; ask the vault owner to clear old copies, then retry', '- "cannot move to trash …" without "100 collisions" — the note stays put; ask the vault owner to fix .trash/ (e.g. a plain file blocks a needed folder), then retry', ...DELETE_FAILURE_ENTRIES, - '- "cannot read trash config from .obsidian/app.json" — the file exists but can\'t be read or parsed; ask the vault owner to repair it, then retry', + '- "cannot read trash config from .obsidian/app.json" — ask the vault owner to repair it, then retry', DAILY_NOTES_CONFIG_ERROR_ENTRY, ].join("\n"), ) @@ -1379,8 +1417,8 @@ describe("vault_memory_recall description reflects EMBEDDING_ENABLED", () => { expect(openerAndReturnsLines(description)).toEqual({ opener: - 'Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries ("testing philosophy", "sustainable pacing") rank best. A meta-framed query ("opinions on testing") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing.', - returns: `${RECALL_RETURNS_LEAD} The search_mode value is "hybrid" when vector matching contributed and "fts" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.`, + 'Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL About Me/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries ("testing philosophy", "sustainable pacing") rank best. A meta-framed query ("opinions on testing") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing.', + returns: `${RECALL_RETURNS_LEAD} search_mode is "hybrid" when semantic matching contributed and "fts" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.`, }) }) @@ -1389,7 +1427,7 @@ describe("vault_memory_recall description reflects EMBEDDING_ENABLED", () => { expect(openerAndReturnsLines(description)).toEqual({ opener: - 'Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. "pacing", then "recovery", then "sustainable hours"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty.', + 'Recall memory entries about a topic — entry-granular keyword retrieval across ALL About Me/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. "pacing", then "recovery", then "sustainable hours"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing.', returns: `${RECALL_RETURNS_LEAD} In keyword-only mode search_mode is always "fts" and reranked always false.`, }) expect(description).not.toContain("hybrid") @@ -1581,18 +1619,16 @@ describe("vault_memory_recall handler", () => { isError?: boolean } expect(result.isError).toBeUndefined() - const payload = JSON.parse(requireTextContent(result)) as { - entries: Array<{ file: string; date: string }> - total: number - truncated: boolean - search_mode: string - } // Three entries match "mutation"; limit: 2 must reach the query // layer (the truncation is only observable if the mapping worked). - expect(payload.total).toBe(3) - expect(payload.truncated).toBe(true) - expect(payload.entries).toHaveLength(2) - expect(payload.search_mode).toBe("fts") + // Which two come back is a ranking detail, not the limit under test. + expect(JSON.parse(requireTextContent(result))).toEqual({ + entries: [expect.any(Object), expect.any(Object)], + total: 3, + truncated: true, + search_mode: "fts", + reranked: false, + }) }) it("returns an empty evidence set for a no-match query, not an error", async () => { @@ -1602,14 +1638,13 @@ describe("vault_memory_recall handler", () => { isError?: boolean } expect(result.isError).toBeUndefined() - const payload = JSON.parse(requireTextContent(result)) as { - entries: unknown[] - total: number - truncated: boolean - } - expect(payload.entries).toEqual([]) - expect(payload.total).toBe(0) - expect(payload.truncated).toBe(false) + expect(JSON.parse(requireTextContent(result))).toEqual({ + entries: [], + total: 0, + truncated: false, + search_mode: "fts", + reranked: false, + }) }) }) @@ -2003,7 +2038,7 @@ describe("vault_find_orphans live folder defaults", () => { ?.split("\n") .find((line) => line.startsWith("- exclude_folders replaces the defaults")) expect(excludeFoldersLine).toBe( - '- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults\' daily notes folder is the one vault_get_daily_note\'s path starts with; when it is the vault root or blank, the defaults hold no daily folder.', + '- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links. The defaults\' daily notes folder is the one vault_get_daily_note\'s path starts with.', ) expect(toolConfig.description).not.toContain("Journal") expect(toolConfig.inputSchema?.shape.exclude_folders?.description).toBe( @@ -2019,11 +2054,11 @@ describe("vault_find_orphans live folder defaults", () => { // daily-folder line can't pass as "left out" const parametersSection = toolConfig.description ?.split("\nParameters:\n")[1] - ?.split("\n\nErrors:")[0] + ?.split("\n\nBehavior:")[0] expect(parametersSection).toBe( [ '- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links.', - "- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check.", + "- limit applies after exclusions and sorting; nothing in the response signals truncation.", ].join("\n"), ) expect(toolConfig.inputSchema?.shape.exclude_folders?.description).toBe( @@ -2949,11 +2984,9 @@ describe("DISABLED_TOOLS", () => { '- exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links.' expect(excludeFoldersEntry("")).toBe( - `${ENTRY_LEAD} The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder.`, - ) - expect(excludeFoldersEntry("vault_get_daily_note")).toBe( - `${ENTRY_LEAD} When the vault's daily notes folder is the vault root or blank, the defaults hold no daily folder.`, + `${ENTRY_LEAD} The defaults' daily notes folder is the one vault_get_daily_note's path starts with.`, ) + expect(excludeFoldersEntry("vault_get_daily_note")).toBe(ENTRY_LEAD) }) it("disabling the memory write tools trims them from memory read-tool descriptions", () => { @@ -2964,7 +2997,16 @@ describe("DISABLED_TOOLS", () => { ([toolName]) => toolName === TOOL_NAMES.VAULT_LIST_MEMORY_FILES, ) const description = listFilesCall?.[1].description - expect(description).toContain("BEFORE calling vault_get_memory.") + + expect( + findDescriptionLine({ + registeredCalls, + toolName: TOOL_NAMES.VAULT_LIST_MEMORY_FILES, + linePrefix: "When to use:", + }), + ).toBe( + "When to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory. To find entries by topic instead, use vault_memory_recall.", + ) expect(description).not.toContain("vault_update_memory") expect(description).not.toContain("pruning entries") }) @@ -2981,8 +3023,14 @@ describe("DISABLED_TOOLS", () => { ) const description = listFilesCall?.[1].description - expect(description).toContain( - "When to use: Discovering what memory files and sections exist — and what each file is for. Always call this first", + expect( + findDescriptionLine({ + registeredCalls, + toolName: TOOL_NAMES.VAULT_LIST_MEMORY_FILES, + linePrefix: "When to use:", + }), + ).toBe( + "When to use: Discovering what memory files and sections exist — and what each file is for. To find entries by topic instead, use vault_memory_recall.", ) expect(description).not.toContain("BEFORE calling") }) @@ -3003,21 +3051,93 @@ describe("DISABLED_TOOLS", () => { linePrefix: "Returns:", }), ).toBe( - 'Returns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included). The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. The search_mode value is "hybrid" when vector matching contributed and "fts" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker\'s relevance cut was applied.', + 'Returns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included). The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is "hybrid" when semantic matching contributed and "fts" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.', ) expect(description).not.toContain("feed directly into") }) + it.each([ + { toolName: TOOL_NAMES.VAULT_LIST_FILES, disabledTool: "vault_read_file" }, + { toolName: TOOL_NAMES.VAULT_GET_DAILY_NOTE, disabledTool: "vault_recent_notes" }, + { toolName: TOOL_NAMES.VAULT_GET_MEMORY, disabledTool: "vault_memory_recall" }, + { toolName: TOOL_NAMES.VAULT_LIST_MEMORY_FILES, disabledTool: "vault_memory_recall" }, + { toolName: TOOL_NAMES.VAULT_DELETE_MEMORY, disabledTool: "vault_delete_note" }, + { toolName: TOOL_NAMES.VAULT_UPDATE_MEMORY, disabledTool: "vault_delete_memory" }, + { toolName: TOOL_NAMES.VAULT_LIST_TAGS, disabledTool: "vault_list_property_values" }, + { toolName: TOOL_NAMES.VAULT_READ_FILE, disabledTool: "vault_read_note" }, + { toolName: TOOL_NAMES.VAULT_READ_FILE, disabledTool: "vault_get_outgoing_links" }, + { toolName: TOOL_NAMES.VAULT_READ_FILE, disabledTool: "vault_list_files" }, + { toolName: TOOL_NAMES.VAULT_READ_FILE, disabledTool: "vault_search" }, + { toolName: TOOL_NAMES.VAULT_LIST_FILES, disabledTool: "vault_get_outgoing_links" }, + ])("$toolName names $disabledTool only while it is served", ({ toolName, disabledTool }) => { + const describeTool = (disabledTools: string): string | undefined => { + const registeredCalls = registerWithConfig({ DISABLED_TOOLS: disabledTools }) + return registeredCalls.find(([registeredName]) => registeredName === toolName)?.[1] + .description + } + + expect(describeTool("")).toContain(disabledTool) + expect(describeTool(disabledTool)).not.toContain(disabledTool) + }) + + it("no served tool's text names a tool DISABLED_TOOLS turned off", () => { + // The input schema as JSON Schema carries every .describe() text, nested + // objects included, the way a client receives it. + const collectServedText = ([toolName, toolConfig]: RegisterToolCall): string => { + if (!toolConfig.inputSchema) throw new Error(`tool ${toolName} registered without a schema`) + + const schemaText = JSON.stringify(z.toJSONSchema(toolConfig.inputSchema)) + return `${requireDescription(toolConfig, toolName)}\n${schemaText}` + } + + // Each entry reads "naming tool -> disabled tool"; \b keeps vault_search + // from matching inside vault_search_by_tag. + const findToolsNaming = (disabledTool: string): string[] => { + const disabledToolPattern = new RegExp(`\\b${disabledTool}\\b`) + const namingCalls = registerWithConfig({ DISABLED_TOOLS: disabledTool }).filter( + (registeredCall) => disabledToolPattern.test(collectServedText(registeredCall)), + ) + return namingCalls.map(([namingTool]) => `${namingTool} -> ${disabledTool}`) + } + + expect(ALL_TOOL_NAMES.flatMap(findToolsNaming)).toEqual([]) + }) + + const listFilesWhenToUse = (disabledTools: string): string | undefined => { + return findDescriptionLine({ + registeredCalls: registerWithConfig({ DISABLED_TOOLS: disabledTools }), + toolName: TOOL_NAMES.VAULT_LIST_FILES, + linePrefix: "When to use:", + }) + } + + it("vault_list_files contrasts itself with the note discovery tools while all three are served", () => { + expect(listFilesWhenToUse("")).toBe( + "When to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.", + ) + }) + + it.each(["vault_list_notes", "vault_search_by_folder", "vault_search"])( + "vault_list_files falls back to its plain purpose when %s is disabled", + (disabledTool) => { + expect(listFilesWhenToUse(disabledTool)).toBe( + "When to use: discovering what files exist before reading them with vault_read_file. This is the discovery surface for every file that is not a markdown note. For the files one specific note links to, prefer vault_get_outgoing_links.", + ) + }, + ) + it("names only the surviving consumer when vault_get_memory alone is disabled", () => { const registeredCalls = registerWithConfig({ DISABLED_TOOLS: "vault_get_memory", }) - const recallCall = registeredCalls.find( - ([toolName]) => toolName === TOOL_NAMES.VAULT_MEMORY_RECALL, - ) - - expect(recallCall?.[1].description).toContain( - "file and section feed directly into vault_delete_memory.", + expect( + findDescriptionLine({ + registeredCalls, + toolName: TOOL_NAMES.VAULT_MEMORY_RECALL, + linePrefix: "Returns:", + }), + ).toBe( + 'Returns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included); file and section feed directly into vault_delete_memory. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file. search_mode is "hybrid" when semantic matching contributed and "fts" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.', ) }) @@ -3277,7 +3397,7 @@ describe("DISABLED_TOOLS", () => { }) expect(whenToUse).toBe( [ - 'When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=""). Replaces text in place; does not move content across sections.', + 'When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text="").', 'To replace a large block by anchors instead of reproducing the full old_text, use vault_replace_span. To relocate content between headings, use vault_patch_note to add at the target first, then remove from source (new_text="") — add-before-delete, so a failure duplicates the block instead of losing it.', ].join("\n"), ) @@ -3296,21 +3416,21 @@ describe("DISABLED_TOOLS", () => { startMarker: '- "start anchor not found"', endMarker: '\n- "ambiguous', expectedEntry: - '- "start anchor not found" / "end anchor not found" — no line contains the fragment (for end_anchor, none at or after the start line); check the fragment\'s letter case and spacing', + '- "start anchor not found" / "end anchor not found" — no body line holds the fragment where it is searched; check the fragment\'s letter case and spacing', }, { toolName: TOOL_NAMES.VAULT_REPLACE_SPAN, startMarker: '- "start anchor not found"', endMarker: '\n- "ambiguous', expectedEntry: - '- "start anchor not found" / "end anchor not found" — no line contains the fragment (for end_anchor, none at or after the start line); check the fragment\'s letter case and spacing', + '- "start anchor not found" / "end anchor not found" — no body line holds the fragment where it is searched; check the fragment\'s letter case and spacing', }, { toolName: TOOL_NAMES.VAULT_INSERT_AT_ANCHOR, startMarker: '- "anchor not found"', endMarker: '\n- "ambiguous', expectedEntry: - '- "anchor not found" — fragment not on any line; check the fragment\'s letter case and spacing', + '- "anchor not found" — fragment not on any body line; check the fragment\'s letter case and spacing', }, ])( "$toolName's not-found entry keeps a remedy without vault_read_note", @@ -3348,38 +3468,44 @@ describe("DISABLED_TOOLS", () => { expect(ambiguousHeadingEntry).toBe(expectedEntry) }) + const WRITE_NOTE_WHEN_TO_USE = + "When to use: Creating a new note, or replacing a whole note's body." + const WRITE_NOTE_PROPERTIES_LINE = + "Prefer vault_update_properties for property-only edits (no body round-trip)." + const WRITE_NOTE_MEMORY_LINE = + "Prefer vault_update_memory for appending dated entries to About Me/ memory files." + it.each([ { label: "names both partial-edit tools while they are served", disabledTools: "", - expectedEntry: - '- "note already exists" — set overwrite: true to replace it, or use vault_patch_note or vault_replace_in_note for partial edits', + partialEditLine: "Prefer vault_patch_note or vault_replace_in_note to change part of a note.", }, { label: "names only vault_patch_note when vault_replace_in_note is disabled", disabledTools: "vault_replace_in_note", - expectedEntry: - '- "note already exists" — set overwrite: true to replace it, or use vault_patch_note for partial edits', + partialEditLine: "Prefer vault_patch_note to change part of a note.", }, { label: "names only vault_replace_in_note when vault_patch_note is disabled", disabledTools: "vault_patch_note", - expectedEntry: - '- "note already exists" — set overwrite: true to replace it, or use vault_replace_in_note for partial edits', - }, - { - label: "offers only overwrite when both partial-edit tools are disabled", - disabledTools: "vault_patch_note,vault_replace_in_note", - expectedEntry: '- "note already exists" — set overwrite: true to replace it', + partialEditLine: "Prefer vault_replace_in_note to change part of a note.", }, - ])("vault_write_note's already-exists entry $label", ({ disabledTools, expectedEntry }) => { - const alreadyExistsEntry = extractDescriptionSection({ + ])("vault_write_note's partial-edit routing $label", ({ disabledTools, partialEditLine }) => { + const whenToUse = extractDescriptionSection({ registeredCalls: registerWithConfig({ DISABLED_TOOLS: disabledTools }), toolName: TOOL_NAMES.VAULT_WRITE_NOTE, - startMarker: '- "note already exists"', - endMarker: '\n- "path must end', + startMarker: "When to use:", + endMarker: "\n\nErrors:", }) - expect(alreadyExistsEntry).toBe(expectedEntry) + expect(whenToUse).toBe( + [ + WRITE_NOTE_WHEN_TO_USE, + partialEditLine, + WRITE_NOTE_PROPERTIES_LINE, + WRITE_NOTE_MEMORY_LINE, + ].join("\n"), + ) }) it.each([ @@ -3411,27 +3537,30 @@ describe("DISABLED_TOOLS", () => { expect(routingLine).toBe(`the full note body.\n${expectedLine}`) }) - const WRITE_NOTE_WHEN_TO_USE = - "When to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body." - const WRITE_NOTE_PROPERTIES_LINE = - "Prefer vault_update_properties for property-only edits (no body round-trip)." - const WRITE_NOTE_MEMORY_LINE = - "Prefer vault_update_memory for appending dated entries to About Me/ memory files." - it.each([ { - label: "keeps both routing lines while their tools are served", + label: "keeps every routing line while its tools are served", disabledTools: "", - expectedLines: [WRITE_NOTE_WHEN_TO_USE, WRITE_NOTE_PROPERTIES_LINE, WRITE_NOTE_MEMORY_LINE], + expectedLines: [ + WRITE_NOTE_WHEN_TO_USE, + "Prefer vault_patch_note or vault_replace_in_note to change part of a note.", + WRITE_NOTE_PROPERTIES_LINE, + WRITE_NOTE_MEMORY_LINE, + ], }, { label: "drops the vault_update_properties line when that tool is disabled", disabledTools: "vault_update_properties", - expectedLines: [WRITE_NOTE_WHEN_TO_USE, WRITE_NOTE_MEMORY_LINE], + expectedLines: [ + WRITE_NOTE_WHEN_TO_USE, + "Prefer vault_patch_note or vault_replace_in_note to change part of a note.", + WRITE_NOTE_MEMORY_LINE, + ], }, { - label: "leaves only the when-to-use line when both routing tools are disabled", - disabledTools: "vault_update_properties,vault_update_memory", + label: "leaves only the when-to-use line when every routing tool is disabled", + disabledTools: + "vault_update_properties,vault_update_memory,vault_patch_note,vault_replace_in_note", expectedLines: [WRITE_NOTE_WHEN_TO_USE], }, ])("vault_write_note's when-to-use $label", ({ disabledTools, expectedLines }) => { @@ -3774,7 +3903,7 @@ describe("DISABLED_TOOLS", () => { toolName: TOOL_NAMES.VAULT_REPLACE_IN_NOTE, disabledTools: "vault_delete_span,vault_replace_span,vault_patch_note", expectedWhenToUse: - 'When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=""). Replaces text in place; does not move content across sections.', + 'When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text="").', }, ])("$label", ({ toolName, disabledTools, expectedWhenToUse }) => { const whenToUse = extractDescriptionSection({ diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index fbc781aa5..dcb9887e0 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -78,7 +78,7 @@ describe("tool surface baseline", () => { * - The allowance sits just above the default combo's average, so only * small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4198 + const CHARS_PER_TOOL_ALLOWANCE = 3997 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/asset-tools.ts b/src/vault-mcp/mcp-core/tools/asset-tools.ts index 03ee493dd..a04f458f9 100644 --- a/src/vault-mcp/mcp-core/tools/asset-tools.ts +++ b/src/vault-mcp/mcp-core/tools/asset-tools.ts @@ -11,6 +11,7 @@ import { describeTextWindow, FILESYSTEM_ERROR_ENTRY, LISTING_FILESYSTEM_ERROR_ENTRY, + PATH_SAFETY_ERROR_ENTRY, } from "./tool-helpers.js" /** One-line, model-facing summary accompanying an image block: what file it @@ -80,15 +81,28 @@ export const registerAssetTools = ({ registerTool, safeHandler, safeHandlerContent, + isToolEnabled, + whenToolEnabledText, vaultPath, logger: sessionLogger, config, }: ToolRegistrationContext): void => { + // The contrast with the note tools names three of them, so it is kept only + // while all three are served; otherwise the plain purpose stands. + const noteDiscoveryToolsServed = [ + TOOL_NAMES.VAULT_LIST_NOTES, + TOOL_NAMES.VAULT_SEARCH_BY_FOLDER, + TOOL_NAMES.VAULT_SEARCH, + ].every(isToolEnabled) + const listFilesContrastSentence = noteDiscoveryToolsServed + ? " vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else." + : " This is the discovery surface for every file that is not a markdown note." + registerTool( TOOL_NAMES.VAULT_READ_FILE, { title: "Read File", - description: `Read a non-markdown vault file in its most useful form per type — the read-side companion to vault_read_note for everything that isn't a note. + description: `Read a non-markdown vault file in its most useful form per type${whenToolEnabledText("vault_read_note", " — the read-side companion to vault_read_note for everything that isn't a note")}. Example: vault_read_file({ path: "attachments/diagram.png" }) — the image itself, shrunk when too large Example: vault_read_file({ path: "Boards/Roadmap.canvas" }) — a readable outline of the canvas @@ -96,37 +110,37 @@ Example: vault_read_file({ path: "exports/data.json" }) — the file content as Example: vault_read_file({ path: "exports/big.csv", limit: 500 }) — the first 500 lines Example: vault_read_file({ path: "papers/research.pdf" }) — structured text with title, headings, and links +When to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF.${whenToolEnabledText("vault_get_outgoing_links", " Find the files a note links to (with byte sizes) via vault_get_outgoing_links.")}${whenToolEnabledText("vault_list_files", " Browse a folder's files via vault_list_files.")}${whenToolEnabledText("vault_search", " vault_search also indexes canvas, PDF, and text-format content, but not images or other files.")}${whenToolEnabledText("vault_read_note", " For .md notes use vault_read_note.")} To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total. + What each file type returns: -- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds the image output budget (MAX_IMAGE_OUTPUT_BYTES, ${config.maxImageOutputBytes} bytes) or 1568 pixels on its longer side, delivered untouched otherwise — plus a text line stating the path, delivered format/dimensions/bytes, and the original dimensions when shrunk. Animated GIFs are reduced to their first frame when recompressed. -- Canvas (.canvas): a readable markdown outline per JSON Canvas 1.0 — groups (by visual containment), node content in reading order, and a connections list with edge labels. -- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy (from font sizes relative to the body text), code blocks and inline code (from monospace fonts), page separators, and a deduplicated links footer. -- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. .svg is returned as its XML source; .base as its YAML source. +- Images (.png/.jpg/.jpeg/.gif/.webp): the image as an image block, downscaled and recompressed when it exceeds the image output budget (${config.maxImageOutputBytes} bytes) or has a longer side over 1568 pixels, and delivered untouched otherwise. Animated GIFs are reduced to their first frame when recompressed. +- Canvas (.canvas): a readable markdown outline — groups, node content in reading order, and a connections list with edge labels. +- PDFs (.pdf): structured text with document metadata — title, page count, heading hierarchy, code blocks and inline code, page separators, and a links footer. +- Text formats (.svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base): the file content verbatim as text. raw: true switches a canvas or PDF to its other form: -- Canvas: the exact JSON source (geometry, ids, colors — full fidelity) instead of the outline. -- PDF: each page rendered and returned as an image block instead of extracted text, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Only the first ${config.maxPdfRenderPages} pages are rendered; later pages are readable only as text, without raw. +- Canvas: the exact JSON source (geometry, ids, colors) instead of the outline. +- PDF: each page rendered as an image block instead of extracted text, showing layout, diagrams, tables, and formatting. Only the first ${config.maxPdfRenderPages} pages are rendered; later pages are readable only as text, without raw. -Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a line window, preceded by a metadata line stating the window, the total line count, and where to continue ("data.csv — lines 51–100 of 400 (continue with start_line: 101)"). The text output cap (100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact. - -When to use: whenever a note references a file you need to actually see or read — an embedded diagram, a linked canvas, data file, or PDF. Find the files a note links to (with byte sizes) via vault_get_outgoing_links; browse a folder's files via vault_list_files. vault_search also indexes canvas, PDF, and text-format content, but not images or other files. For .md notes use vault_read_note. To check a large file's line count before reading it whole, request start_line: 1 with limit: 1 — one line plus the total. +Line paging: start_line and limit page any text result as a line window, preceded by a metadata line stating the window, the total line count, and where to continue ("data.csv — lines 51–100 of 400 (continue with start_line: 101)"). The text output cap (a fixed 100 KiB) applies to each window, so one very long line can still overflow it; paging never gets around the file-size cap. Paged windows come back with \\n line endings and no trailing newline; an unpaged read of a text format returns the file byte-exact. Errors: -- "not a file: … is a markdown note" — the path ends in .md; read notes with vault_read_note -- "file not found" — no file exists at that path (a folder is not a file); discover valid paths via vault_list_files -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it (hidden files are not readable) +- "not a file: … is a markdown note" — the path ends in .md${whenToolEnabledText("vault_read_note", "; read notes with vault_read_note")} +- "file not found" — no file exists at that path (a folder is not a file)${whenToolEnabledText("vault_list_files", "; discover valid paths via vault_list_files")} +${PATH_SAFETY_ERROR_ENTRY} - "file too large" — the file exceeds the file-size cap (MAX_FILE_BYTES, default 50 MiB), which the server's owner can raise - "text output too large" — a text file, canvas, or PDF renders past the text output cap; page it with start_line and limit, or reduce limit when a single window overflows - "start line past the end" — start_line exceeds the file's line count; the error states the total, so retry with a smaller start_line -- "line range is not available" (start_line or limit on an image, or on a PDF read with raw: true) / "raw source is not available for images" (raw on an image) — drop that input; paging applies only to text results, and an image always comes back as its image block +- "line range is not available" (start_line or limit on an image, or on a PDF read with raw: true) / "raw source is not available for images" (raw on an image) — drop that input - "not valid UTF-8" — the file's bytes aren't UTF-8 text; ask for a UTF-8 copy -- "invalid .canvas JSON" — the canvas file is empty or not valid JSON, so no outline can be built; set raw: true to read its source as text +- "invalid .canvas JSON" — the canvas file is empty or not valid JSON; set raw: true to read its source as text - "PDF has no extractable text" — the PDF contains no text (scanned or image-only); the error states the page count. Set raw: true to render pages as images instead -- "PDF is password-protected" — the PDF needs a password, which this tool cannot supply; read an unprotected copy instead -- "PDF is damaged or not a PDF" / "could not decode image" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; replace or re-export the file -- "PDF page rendering failed" — raw: true was set and the PDF opened, but no page could be rendered; read it without raw: true to get its text instead. If that read also finds no text, this tool can't read the PDF -- "image cannot be fitted" — the image could not be compressed under the image output budget; ask for a smaller export +- "PDF is password-protected" — this tool cannot supply a password; read an unprotected copy instead +- "PDF is damaged or not a PDF" / "could not decode image" — the PDF does not parse, or the image is empty, damaged, not an image despite its extension, or over about 268 million pixels; any other PDF or image error comes back in the reading library's own words. Replace or re-export the file +- "PDF page rendering failed" — read it without raw: true to get its text instead; if that read also finds no text, this tool can't read the PDF +- "image cannot be fitted into …" — the image is over the image output budget even after shrinking; ask the server's owner to raise MAX_IMAGE_OUTPUT_BYTES ${FILESYSTEM_ERROR_ENTRY} -- "file changed while reading" — the file was written while it was read (for example by Sync); retry the read +- "file changed while reading" — the file was written while it was read; retry the read - "PDF reader could not start" — the server's PDF reader failed to load; retry once, then ask the vault's owner to restart the server - "unsupported file type …" — the type (audio, archives, …) is not readable; the error names the readable types plus the file's existence and size @@ -227,18 +241,19 @@ Example: vault_list_files({}) — non-markdown files across the whole vault Example: vault_list_files({ folder: "attachments" }) Example: vault_list_files({ extensions: [".png", ".jpg"], limit: 20 }) -When to use: discovering what files exist before reading them with vault_read_file. vault_list_notes and vault_search_by_folder cover only markdown notes, and beyond notes vault_search indexes only canvas, PDF, and text-format files, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links. +When to use: discovering what files exist${whenToolEnabledText("vault_read_file", " before reading them with vault_read_file")}.${listFilesContrastSentence}${whenToolEnabledText("vault_get_outgoing_links", " For the files one specific note links to, prefer vault_get_outgoing_links.")} Behavior: Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. Hidden (dot-prefixed) files and folders, .obsidian/ among them, never appear. Errors: -- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error. -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — the folder starts at the filesystem root, escapes the vault (e.g. "../elsewhere") or names the vault root itself (e.g. "."), or is hidden like ".obsidian" (hidden folders are not listable); use a vault-relative folder outside hidden folders, and omit folder to list the whole vault. +- A visible folder containing no files — or one that doesn't exist, or a path that names a file — returns an empty listing, not an error. +- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — the folder is absolute, escapes the vault, names the vault root itself (e.g. "."), or is hidden like ".obsidian"; use a vault-relative folder outside hidden folders, and omit folder to list the whole vault. ${LISTING_FILESYSTEM_ERROR_ENTRY}. -Returns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is "(none)" for a file without one), extension_counts (counts keyed by extension, e.g. { ".png": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). - -Reading listed files: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base; other listed types are not readable. The bytes value is the on-disk size, not the delivery cost. An image comes back shrunk to fit when needed, so a large listed image is still cheap to read. A text format returns verbatim, and one over 100 KiB must be read in windows with vault_read_file's start_line and limit.`, +Returns: JSON with files (array of { path, extension, bytes }, sorted by path; extension is "(none)" for a file without one), extension_counts (counts keyed by extension, e.g. { ".png": 3 }, over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit).${whenToolEnabledText( + "vault_read_file", + "\n\nReadable types: vault_read_file reads images (.png/.jpg/.jpeg/.gif/.webp), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read any type outside these lists.", + )}`, inputSchema: { folder: z .string() diff --git a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts index 016c1a37e..32e3f34eb 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -9,7 +9,6 @@ import { FILESYSTEM_ERROR_ENTRY } from "./tool-helpers.js" export const registerDailyNoteTools = ({ registerTool, safeHandler, - isToolEnabled, whenToolEnabledText, vaultPath, logger: sessionLogger, @@ -24,19 +23,19 @@ export const registerDailyNoteTools = ({ Example: vault_get_daily_note({ date: "2026-05-13" }) Example: vault_get_daily_note({}) — returns today's daily note -When to use: When you need today's or a specific date's daily note. ${whenToolEnabledText("vault_patch_note", "To append content to a daily note section, use the returned path with vault_patch_note. ")}Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes). +When to use: When you need today's or a specific date's daily note.${whenToolEnabledText("vault_patch_note", " To append content to a daily note section, use the returned path with vault_patch_note.")}${whenToolEnabledText("vault_recent_notes", " For recent vault activity, vault_recent_notes lists the most recently changed notes; it has no date filter.")} Parameters: -- date: past and future dates are both valid. +- date: always YYYY-MM-DD, whatever filename format the vault uses; past and future dates are both valid. Errors: - "invalid date" — use YYYY-MM-DD format -- "… is not a calendar date" — the date has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date -- "daily note format contains unsupported token(s): ..." — the configured format uses tokens the server cannot reproduce (ordinals like Do/Mo, dd, d, e, k/kk, w, Q, Z/ZZ, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative +- "… is not a calendar date" — the date names a day that doesn't exist, such as 2026-02-30; pass a real date +- "daily note format contains unsupported token(s): ..." — the configured format uses tokens the server cannot reproduce; the message names the tokens and the fix - "path traversal blocked" / "hidden path blocked" / "absolute path blocked" — the configured daily notes folder or format leads outside the vault or into a hidden folder; change it in Obsidian, or set DAILY_NOTES_FOLDER and DAILY_NOTES_FORMAT ${FILESYSTEM_ERROR_ENTRY} -Returns: JSON with path (string — resolved vault-relative path), content (string|null — full note body, or null when the note doesn't exist), and exists (boolean).${isToolEnabled("vault_write_note") ? " When exists is false, create the note with vault_write_note using the returned path." : ""}`, +Returns: JSON with path (string — resolved vault-relative path), content (string|null — the note's full text, properties block included, or null when the note doesn't exist), and exists (boolean).${whenToolEnabledText("vault_write_note", " When exists is false, create the note with vault_write_note using the returned path.")}`, inputSchema: { date: z .string() diff --git a/src/vault-mcp/mcp-core/tools/memory-tools.ts b/src/vault-mcp/mcp-core/tools/memory-tools.ts index 2839e06f7..57923f0ea 100644 --- a/src/vault-mcp/mcp-core/tools/memory-tools.ts +++ b/src/vault-mcp/mcp-core/tools/memory-tools.ts @@ -7,10 +7,16 @@ import type { ToolRegistrationContext } from "./tool-helpers.js" import { createStrictInputObject, FILESYSTEM_ERROR_ENTRY, + formatServedSentencesLine, INDEX_ENGINE_ERROR_ENTRY, toolInputErrorResult, } from "./tool-helpers.js" +/** One entry for the bare-name checks every tool taking a memory file name + * runs, so the three descriptions cannot drift apart. */ +const MEMORY_FILE_NAME_ERROR_ENTRY = + '- "memory file must not start with a dot" / "memory file must be a bare name without path separators" / "memory file must be named without .md" — each message names the part of the name to drop' + /** The memory store names a folder standing where the file would go. That * folder is the vault owner's, so the caller picks another name instead of * removing it. */ @@ -22,10 +28,8 @@ const MEMORY_FILE_IS_FOLDER_ERROR_ENTRY = const MEMORY_FOLDER_ERROR_ENTRY = '- "cannot create memory folder …" — a file sits where the memory folder belongs; ask the vault\'s owner to move it' -/** The memory store wraps a failed read without the Node error's code, so the - * filesystem-error entry's code names never match this message. */ -const MEMORY_FILE_READ_ERROR_ENTRY = - "- \"cannot read memory file …\" — the file can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise its permissions need the vault's owner" +const SECTION_NOT_FOUND_ERROR_ENTRY = + '- "section not found: …" — no H2 heading matches; the message lists the file\'s sections' /** A memory write keeps the file's properties block, so it refuses only a * file with no block whose opening lines would become one once saved: @@ -38,6 +42,7 @@ export const registerMemoryTools = ({ registerTool, safeHandler, formatEnabledToolList, + isToolEnabled, whenToolEnabledText, vaultPath, search, @@ -46,6 +51,20 @@ export const registerMemoryTools = ({ }: ToolRegistrationContext): void => { const memoryStore = createMemoryStore({ memoryDir: config.memoryDir }) + // A wrong file name's remedy names vault_list_memory_files only while it is + // served; otherwise it falls back to a check that needs no tool. + const listMemoryFilesEnabled = isToolEnabled("vault_list_memory_files") + const memoryFileNotFoundRemedy = listMemoryFilesEnabled + ? "call vault_list_memory_files to discover valid names" + : "check file's spelling and letter case" + const memoryFileNameCheckRemedy = listMemoryFilesEnabled + ? "check file against vault_list_memory_files and shorten an over-long name" + : "check file's spelling and length" + + // The memory store wraps a failed read without the Node error's code, so the + // filesystem-error entry's code names never match this message. + const memoryFileReadErrorEntry = `- "cannot read memory file …" — the file can't be read: ${memoryFileNameCheckRemedy}; otherwise ask the vault's owner to fix its permissions` + // Tools a reader follows vault_list_memory_files with. Every one of them — // the read tool included — can be dropped individually via DISABLED_TOOLS, // so the purpose sentence falls back to the discovery value alone rather @@ -56,8 +75,8 @@ export const registerMemoryTools = ({ TOOL_NAMES.VAULT_DELETE_MEMORY, ]) const discoveryPurpose = discoveryFollowUpTools - ? `Discovering what memory files and sections exist — and what each file is for — BEFORE calling ${discoveryFollowUpTools}.` - : "Discovering what memory files and sections exist — and what each file is for." + ? `Discovering what memory files and sections exist — and what each file is for — BEFORE calling ${discoveryFollowUpTools}` + : "Discovering what memory files and sections exist — and what each file is for" // The tools a recall entry's file/section fields feed into. Rendered as a // trailing clause (semicolon included) so the surrounding sentence keeps its @@ -70,31 +89,38 @@ export const registerMemoryTools = ({ ? `; file and section feed directly into ${recallConsumerTools}` : "" + const memoryReadAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText("vault_read_note", "Prefer vault_read_note for reading non-memory notes."), + whenToolEnabledText( + "vault_memory_recall", + "To find entries by topic across every file, use vault_memory_recall.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_GET_MEMORY, { title: "Get Memory", - description: `Read semantic memory from ${config.memoryDir}/ files. These are structured memory files containing dated bullet entries organized under H2 headings. With file: that file's content, frontmatter stripped. With file+section: just that H2 section's entries. No args: every file's content, frontmatter stripped, joined by --- lines — can be large. Returns empty string when no memory files exist yet. + description: `Read semantic memory from ${config.memoryDir}/ files. Each file holds dated bullet entries under H2 headings. With file: that file. With file+section: just that H2 section's entries. No args: every file, joined by --- lines — can be large. -With file+on_or_after: returns structured JSON entries dated on or after the boundary date (inclusive), in document order (newest first when new entries go at the top, their default position). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution. +With file+on_or_after: only the entries dated on or after that date, in document order (newest first by default). Add section to scope to one H2 section; omit it to read every H2 section's entries in the file, each keeping its section attribution. Example: vault_get_memory({ file: "Principles" }) — the whole file Example: vault_get_memory({ file: "Principles", section: "Decision heuristics (newest first)" }) Example: vault_get_memory({ file: "Opinions", section: "Code patterns", on_or_after: "2026-09-01" }) Example: vault_get_memory({ file: "Opinions", on_or_after: "2026-09-01" }) — every section's entries since the date -When to use: Reading user preferences, principles, opinions, or other persistent context stored in ${config.memoryDir}/ files. Call vault_list_memory_files first to discover valid file and section names. Use on_or_after when you need entries from a known date forward. -Prefer vault_read_note for reading non-memory notes. +When to use: Reading user preferences, principles, opinions, or other persistent context stored in ${config.memoryDir}/ files.${whenToolEnabledText("vault_list_memory_files", " Call vault_list_memory_files first to discover valid file and section names.")}${memoryReadAlternativesLine} Errors: -- "section requires a file" / "on_or_after requires a file" — section or on_or_after was passed without file; add file -- "memory file not found" — file does not exist in ${config.memoryDir}/; call vault_list_memory_files to discover valid names -- "memory file must not start with a dot" / "memory file must be a bare name without path separators" / "memory file must be named without .md" — pass the file's bare name: no folder or slash, no leading dot, and no .md -- "section not found: …" — no H2 heading matches; the error lists the file's available sections +- "section requires a file" / "on_or_after requires a file" — add file +- "memory file not found" — ${memoryFileNotFoundRemedy} +${MEMORY_FILE_NAME_ERROR_ENTRY} +${SECTION_NOT_FOUND_ERROR_ENTRY} - "date must be a real ISO calendar date" — on_or_after must be a valid YYYY-MM-DD date -- "cannot read memory file …" / "cannot list memory folder …" — a memory file, or the ${config.memoryDir}/ folder itself on a call without file, can't be read: check file against vault_list_memory_files (a name longer than the filesystem allows fails here); otherwise ask the vault's owner to check the folder +- "cannot read memory file …" / "cannot list memory folder …" — a memory file, or the ${config.memoryDir}/ folder itself on a call without file, can't be read: ${memoryFileNameCheckRemedy}; otherwise ask the vault's owner to check the folder -Returns: Without on_or_after, raw markdown text. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the full raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.`, +Returns: Without on_or_after, raw markdown with the properties block stripped, or an empty string when no memory files exist yet. With on_or_after, JSON { entries, total, on_or_after } where each entry is { file, section, date, text } — text is the raw entry markdown (bullet + continuation lines, wikilinks intact). An empty match returns { entries: [], total: 0, on_or_after }.`, inputSchema: { file: z .string() @@ -108,13 +134,9 @@ Returns: Without on_or_after, raw markdown text. With on_or_after, JSON { entrie .min(1) .optional() .describe( - 'H2 section heading (e.g. "Decision heuristics (newest first)"). Matched case-insensitively, with or without the "(newest first)" suffix. Omit to read the whole file.', + 'H2 section heading (e.g. "Decision heuristics (newest first)"). Matched case-insensitively, with or without the "(newest first)" suffix.', ), - on_or_after: z - .string() - .min(1) - .optional() - .describe("Inclusive date filter (YYYY-MM-DD). Requires a file."), + on_or_after: z.string().min(1).optional().describe("Inclusive date filter (YYYY-MM-DD)."), }, }, async ({ file, section, on_or_after: onOrAfter }, extra) => { @@ -175,20 +197,20 @@ Returns: Without on_or_after, raw markdown text. With on_or_after, JSON { entrie TOOL_NAMES.VAULT_LIST_MEMORY_FILES, { title: "List Memory Files", - description: `Discovery tool — lists ${config.memoryDir}/ memory files with each file's heading outline, per-section entry counts, and leading callout. An entry is a dated bullet line ("- **YYYY-MM-DD**: text"). Does NOT return actual entries. + description: `Discovery tool — lists ${config.memoryDir}/ memory files with each file's heading outline, per-section entry counts, and leading callout. Does NOT return actual entries. -Example: vault_list_memory_files() returns [{ file: "Principles", title: "Principles", bytes: 2048, entry_policy: "append-only", leading_callout: null, headings: [{ level: 2, text: "Decision heuristics (newest first)", entry_count: 12 }] }, ...] +Example: vault_list_memory_files() -When to use: ${discoveryPurpose} Always call this first to get valid file and section names${whenToolEnabledText("vault_delete_memory", ", and to check a file's entry_policy before pruning entries")}. +When to use: ${discoveryPurpose}${whenToolEnabledText("vault_delete_memory", ", and each file's entry_policy to check before pruning entries")}.${whenToolEnabledText("vault_memory_recall", " To find entries by topic instead, use vault_memory_recall.")} -Behavior: Lists every .md file directly inside ${config.memoryDir}/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped. +Behavior: Lists every .md file directly inside ${config.memoryDir}/, sorted by file name. Subfolders are not read, and hidden (dot-prefixed) files are skipped. Errors: - An empty or nonexistent memory folder returns an empty array, not an error. -- "cannot list memory folder …" — a file sits where the memory folder belongs, or its permissions need the vault's owner +- "cannot list memory folder …" — a file stands at the memory folder's path, or its permissions block the server; ask the vault's owner to fix it - "cannot read memory file …" — one listed file can't be read, which fails the whole listing; retry once, then ask the vault's owner to check that file -Returns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); bytes is the on-disk file size; headings lists H1 and H2 headings in order, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a "Scope of this file" block describing what belongs in the file), or null; entry_policy is "append-only" (the default: by convention, entries are never edited or deleted) or "living" (a current-state file whose expired entries may be pruned; declared via \`entry-policy\` frontmatter). The server does not enforce either policy.`, +Returns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — file is the name the other memory tools take as file (no .md); title is the title property, or file when there is none; bytes is the on-disk file size; headings lists H1 and H2 headings in order, each { level, text }, with entry_count on H2s; leading_callout is the top-of-file callout ({ type, title, body }; by convention a "Scope of this file" block describing what belongs in the file), or null; entry_policy is "append-only" (the default: by convention, entries are never edited or deleted) or "living" (a current-state file whose expired entries may be pruned; declared via \`entry-policy\` frontmatter). The server does not enforce either policy.`, inputSchema: {}, }, async (_args, extra) => { @@ -208,16 +230,31 @@ Returns: JSON array of file outlines, each { file, title, bytes, entry_policy, l }, ) + // The two alternatives share one "Prefer …; prefer …" sentence, so each + // clause joins it only while its tool is served. + const recallAlternatives = [ + whenToolEnabledText( + "vault_get_memory", + "vault_get_memory to read a known file or section verbatim", + ), + whenToolEnabledText("vault_search", "vault_search for notes outside the memory layer"), + ] + .filter(Boolean) + .join("; prefer ") + const recallAlternativesSentence = recallAlternatives ? ` Prefer ${recallAlternatives}.` : "" + // These parts read the same with or without embeddings, so both // vault_memory_recall description variants share them const recallUsageSections = `Example: vault_memory_recall({ query: "working hours and pacing" }) Example: vault_memory_recall({ query: "testing philosophy", file: "Opinions" }) -When to use: Answering "what does my memory say about X?" or "how has my view on Y evolved?" — topic-based recall across memory files. Prefer vault_get_memory to read a known file or section verbatim; prefer vault_search for notes outside the memory layer. +When to use: Answering "what does my memory say about X?" or "how has my view on Y evolved?" — topic-based recall across memory files.${recallAlternativesSentence} + +Behavior: A memory file change takes a few seconds to appear here, so an entry written moments ago may not be recalled yet. Errors: -- No matching entries returns { entries: [], total: 0 }, not an error -- An unknown file returns empty results — call vault_list_memory_files to discover valid names +- No matching entries returns entries: [] and total: 0, not an error +- An unknown file returns empty results — ${memoryFileNotFoundRemedy} ${INDEX_ENGINE_ERROR_ENTRY}` const recallReturnsLead = `Returns: JSON { entries, total, truncated, search_mode, reranked }. Each entry is { file, section, date, text } — text is the raw entry markdown (wikilinks intact, continuation lines included)${recallConsumerClause}. The total counts every matched entry; truncated=true means limit dropped the least-relevant matches, whatever their dates, so raise limit or narrow the query for the complete set. The file filter is applied before limit, so limit keeps the most relevant matches from that file.` @@ -226,12 +263,12 @@ ${INDEX_ENGINE_ERROR_ENTRY}` // (EMBEDDING_ENABLED=false) get an honest variant with a synonym-requery // workaround instead of an overclaim. const memoryRecallDescription = config.embeddingEnabled - ? `Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL ${config.memoryDir}/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries and judge relevance yourself when synthesizing an answer. Content-word queries ("testing philosophy", "sustainable pacing") rank best. A meta-framed query ("opinions on testing") that scores below the relevance threshold degrades to relaxed any-term keyword matching instead of returning nothing. + ? `Recall memory entries about a topic — entry-granular hybrid (keyword + semantic) retrieval across ALL ${config.memoryDir}/ files and ALL time. Returns the relevant dated entries, up to limit, sorted oldest-first, so the full evolution of a preference, opinion, or fact is visible — semantic matching finds early entries even when their phrasing differs from the query. Tuned for recall over precision: expect some marginal entries. Content-word queries ("testing philosophy", "sustainable pacing") rank best. A meta-framed query ("opinions on testing") that scores below the relevance threshold degrades to any-term keyword matching instead of returning nothing. ${recallUsageSections} -${recallReturnsLead} The search_mode value is "hybrid" when vector matching contributed and "fts" when the entries came from keyword matching alone — including the any-term fallback; reranked is true when the reranker's relevance cut was applied.` - : `Recall memory entries about a topic — entry-granular keyword retrieval across ALL ${config.memoryDir}/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. "pacing", then "recovery", then "sustainable hours"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching before returning empty. +${recallReturnsLead} search_mode is "hybrid" when semantic matching contributed and "fts" when keyword matching alone did, the any-term fallback included; reranked is true when the reranker ran.` + : `Recall memory entries about a topic — entry-granular keyword retrieval across ALL ${config.memoryDir}/ files and ALL time. Returns the matching dated entries, up to limit, sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off), and phrasing drifts across months, so re-query with synonyms to cover a topic fully (e.g. "pacing", then "recovery", then "sustainable hours"). A multi-word query whose terms never co-occur in one entry degrades to any-term matching instead of returning nothing. ${recallUsageSections} @@ -256,7 +293,7 @@ ${recallReturnsLead} In keyword-only mode search_mode is always "fts" and rerank .min(1) .optional() .describe( - 'Optional: restrict to one memory file, name without .md (e.g. "Opinions"), in its exact letter case. Omit for cross-file recall — the default and usual choice.', + 'Restrict to one memory file, name without .md (e.g. "Opinions"), in its exact letter case. Omit for cross-file recall — the default and usual choice.', ), limit: z .number() @@ -293,17 +330,32 @@ ${recallReturnsLead} In keyword-only mode search_mode is always "fts" and rerank ) }, ) + + const updateMemoryReuseNamesSentence = listMemoryFilesEnabled + ? "Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped." + : "Reuse existing file and section names so entries stay grouped." + const updateMemoryAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_write_note", + "Prefer vault_write_note for creating non-memory notes.", + ), + "A missing file or section is created automatically.", + ]) + + const createdFileReplyReplaceStep = isToolEnabled("vault_replace_in_note") + ? "use vault_replace_in_note to replace" + : "replace" + registerTool( TOOL_NAMES.VAULT_UPDATE_MEMORY, { title: "Update Memory", - description: `Append a dated entry to a section of a memory file in ${config.memoryDir}/. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare \`entry-policy: living\` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation. + description: `Append a dated entry to a section of a memory file in ${config.memoryDir}/. An exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare \`entry-policy: living\` in frontmatter${whenToolEnabledText("vault_list_memory_files", " (surfaced by vault_list_memory_files)")} — a current-state file where pruning expired entries${whenToolEnabledText("vault_delete_memory", " with vault_delete_memory")} is expected maintenance. Example: vault_update_memory({ file: "Opinions", section: "Code patterns (newest first)", entry: "Prefer immutable data structures" }) Example: vault_update_memory({ file: "Routines", section: "Daily/weekly rhythm (newest first)", entry: "Gym Mon/Wed at 6am", options: { date: "2026-10-01", position: "bottom" } }) — a backdated entry placed below the existing ones -When to use: Recording a new preference, principle, opinion, or fact about the user. Call vault_list_memory_files first and reuse existing file and section names so entries stay grouped. -Prefer vault_write_note for creating non-memory notes. A missing file or section is created automatically (new sections get "(newest first)" appended; new files get a placeholder scope callout to fill in via vault_replace_in_note). +When to use: Recording a new preference, principle, opinion, or fact about the user. ${updateMemoryReuseNamesSentence}${updateMemoryAlternativesLine} Parameters: - options.date only sets the bullet's date; placement follows options.position, never the date. @@ -311,13 +363,12 @@ Parameters: Obsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional. Errors: -- "entry must be a single line" — memory entries are single dated bullets; collapse newlines or append multiple entries. -- "section must be a single line" — section names become H2 headings; remove line breaks. -- "date must be a real ISO calendar date" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. "2026-07-02"), not a timestamp. -- "entry contains a control character" / "section contains a control character" — the value includes a non-printable control byte; remove it before writing. -- "memory file must not start with a dot" / "memory file must be a bare name without path separators" / "memory file must be named without .md" — use a bare file name: no folder or slash, no leading dot, and no .md. -- "section not created: … is nearly identical to existing section …" — the name differs from an existing heading only by an HTML-entity slip, a typo, or spacing; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section. Names differing only in digits (e.g. "2025" vs "2026") count as different. -${MEMORY_FILE_READ_ERROR_ENTRY} +- "entry must be a single line" / "section must be a single line" — the message names the fix. +- "date must be a real ISO calendar date" — pass options.date as an existing calendar date in bare YYYY-MM-DD form, not a timestamp. +- "entry contains a control character" / "section contains a control character" — remove the character the message names. +${MEMORY_FILE_NAME_ERROR_ENTRY} +- "section not created: … is nearly identical to existing section …" — pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a new section. +${memoryFileReadErrorEntry} ${MEMORY_FILE_IS_FOLDER_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${MEMORY_FOLDER_ERROR_ENTRY} @@ -341,7 +392,7 @@ Returns: "Added entry to ${config.memoryDir}/.md → ##
", plus a .string() .min(1) .describe( - 'Raw entry text — a single line; the server prepends "- **YYYY-MM-DD**: " automatically. Do not include the date or bullet prefix.', + 'Raw entry text — a single line, without the "- **YYYY-MM-DD**: " prefix, which the server prepends.', ), options: createStrictInputObject({ date: z @@ -390,7 +441,7 @@ Returns: "Added entry to ${config.memoryDir}/.md → ##
", plus a // Nudge the caller to author the scope callout the new file was // seeded with, so the file self-documents what belongs in it. case "created-file": - return `${confirmation}. Created a new memory file with a placeholder scope callout — use vault_replace_in_note to replace its "(describe what belongs in this file — and what doesn't)" placeholder with what the file contains, so other agents know what it is for.` + return `${confirmation}. Created a new memory file with a placeholder scope callout — ${createdFileReplyReplaceStep} its "(describe what belongs in this file — and what doesn't)" placeholder with what the file contains, so other agents know what it is for.` case "created-section": case "appended": return confirmation @@ -400,6 +451,10 @@ Returns: "Added entry to ${config.memoryDir}/.md → ##
", plus a }, ) + const entryNotFoundRemedy = isToolEnabled("vault_get_memory") + ? "verify the exact text with vault_get_memory(file, section)" + : "check entry's letter case and spacing" + registerTool( TOOL_NAMES.VAULT_DELETE_MEMORY, { @@ -409,22 +464,21 @@ Returns: "Added entry to ${config.memoryDir}/.md → ##
", plus a Example: vault_delete_memory({ file: "Opinions", section: "Tools and workflows (newest first)", date: "2026-05-01", entry: "Prefer X over Y" }) Example: vault_delete_memory({ file: "Routines", section: "Daily/weekly rhythm (newest first)", date: "2026-03-10", entry: "Gym Tue/Thu at 7am" }) — prune an expired entry from an entry-policy: living file -When to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state via vault_update_memory instead (newest-first naturally supersedes). The exception is a file whose frontmatter declares \`entry-policy: living\` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching. -Prefer vault_delete_note for deleting entire non-protected notes. - +When to use: Removing an entry that was wrong when it was written. Memory files are append-only by default, so do NOT delete to reflect a change: append the new state${whenToolEnabledText("vault_update_memory", " via vault_update_memory")} instead. The exception is a file whose frontmatter declares \`entry-policy: living\`${whenToolEnabledText("vault_list_memory_files", " (check via vault_list_memory_files)")} — a current-state file where deleting an expired entry is the intended maintenance.${whenToolEnabledText("vault_get_memory", " Call vault_get_memory(file, section) first to see exact entry text for matching.")} +${whenToolEnabledText("vault_delete_note", "Prefer vault_delete_note for deleting whole notes; vault_delete_note refuses protected paths.\n")} Parameters: -- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. +- date + entry together pick the bullet line within the given section; identical copies make the match ambiguous. The entry text matches the bullet's first line only, and the delete also removes the lines that continue that entry. - section scopes the match — an identical entry under a different heading is not found. Errors: -- "memory file not found" — file does not exist in ${config.memoryDir}/; call vault_list_memory_files to discover valid names. -- "memory file must not start with a dot" / "memory file must be a bare name without path separators" / "memory file must be named without .md" — pass the file's bare name: no folder or slash, no leading dot, and no .md. +- "memory file not found" — ${memoryFileNotFoundRemedy}. +${MEMORY_FILE_NAME_ERROR_ENTRY} - "date must be YYYY-MM-DD" — pass the entry's date exactly as the file shows it, zero-padded. -- "section not found: …" — no H2 heading matches; the error lists the file's available sections -- "no entry matching …" — no bullet matched the given date and entry text; verify exact text via vault_get_memory(file, section). -- "ambiguous: N entries match …" — more than one identical bullet exists in the section. Remove the extra copy with ${whenToolEnabledText("vault_delete_span", "vault_delete_span (pass first_match: true — identical lines make every anchor ambiguous) or ")}a manual edit, then retry. -- "refusing memory write: … would shrink content" — safety guard blocked a write that would remove more than half the file. Re-read with vault_get_memory to confirm current content; an entry that really is that large needs ${whenToolEnabledText("vault_delete_span", "vault_delete_span or ")}a manual edit. -${MEMORY_FILE_READ_ERROR_ENTRY} +${SECTION_NOT_FOUND_ERROR_ENTRY} +- "no entry matching …" — ${entryNotFoundRemedy}. +- "ambiguous: N entries match …" — remove the extra copy with ${whenToolEnabledText("vault_delete_span", "vault_delete_span (first_match: true) or ")}a manual edit, then retry. +- "refusing memory write: … would shrink content" — the delete would remove more than half the file; ${whenToolEnabledText("vault_get_memory", "re-read it with vault_get_memory, and ")}remove an entry that really is that large with ${whenToolEnabledText("vault_delete_span", "vault_delete_span or ")}a manual edit. +${memoryFileReadErrorEntry} ${FILESYSTEM_ERROR_ENTRY} ${MEMORY_OPENING_BLOCK_ERROR_ENTRY} diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index ab81fe2c9..bf2941689 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -9,10 +9,14 @@ import { createStrictInputObject, formatNoteMetadata, dateFilterSchema, + formatServedSentencesLine, INDEX_ENGINE_ERROR_ENTRY, INDEX_ERROR_ENTRY, } from "./tool-helpers.js" +/** The link-graph tools accept a canvas as well as a note. */ +const LINK_GRAPH_EXTENSION_ERROR_ENTRY = '- "path must end in …" — add the .md or .canvas extension' + export const registerSearchTools = ({ registerTool, safeHandler, @@ -26,7 +30,7 @@ export const registerSearchTools = ({ }: ToolRegistrationContext): void => { // These parts read the same with or without embeddings, so both // vault_search description variants share them - const searchSyntaxText = `Unquoted terms use implicit AND with stemming ("running" also matches "run"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases automatically.` + const searchSyntaxText = `Unquoted terms use implicit AND with stemming ("running" also matches "run"); wrap in double quotes for exact phrases; punctuated terms (vault-cortex, deploy/local) are matched as exact adjacent-word phrases.` // File-content search applies only the folder filter, so hybrid search // drops file results whenever any other filter is set @@ -43,8 +47,21 @@ export const registerSearchTools = ({ Example: vault_search({ query: "meeting notes", filters: { type: "meeting", folder: "Work" } }) Example: vault_search({ query: "decision", filters: { modified: { after: "2026-06-30" } } }) — matching notes touched in July or later` - const searchAlternativesLine = - "Prefer vault_search_by_tag for tag-only queries without text. Prefer vault_search_by_folder for browsing a folder. Prefer vault_search_by_property for metadata-only queries. Prefer vault_recent_notes for time-based browsing." + const searchAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_search_by_tag", + "Prefer vault_search_by_tag for tag-only queries without text.", + ), + whenToolEnabledText( + "vault_search_by_folder", + "Prefer vault_search_by_folder for browsing a folder.", + ), + whenToolEnabledText( + "vault_search_by_property", + "Prefer vault_search_by_property for metadata-only queries.", + ), + whenToolEnabledText("vault_recent_notes", "Prefer vault_recent_notes for time-based browsing."), + ]) // File content is searched only while FILE_TOOLS_ENABLED is on, so the text // that mentions file results follows that setting, not any one tool @@ -52,10 +69,10 @@ Example: vault_search({ query: "decision", filters: { modified: { after: "2026-0 ? "vault notes and file content (canvas, PDF, and text files)" : "vault notes" const searchResultFieldsText = config.fileToolsEnabled - ? "path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes" - : "path, title, snippet, score, tags, folder, type, kind, created, modified, bytes" + ? "path, title, snippet, score, tags, folder (a note's top-level folder; a file's full parent folder), type, kind, extension, created, modified, bytes" + : "path, title, snippet, score, tags, folder (top-level folder), type, kind, created, modified, bytes" const searchResultKindText = config.fileToolsEnabled - ? `The kind field is "note" for markdown notes or "file" for non-markdown content (.canvas, .pdf, .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. ".pdf").` + ? `The kind field is "note" for markdown notes or "file" for non-markdown content; file results also carry extension (e.g. ".pdf").` : `The kind field is always "note".` const searchResultDetailsText = `${searchResultKindText} The created field is omitted, not null, when a result has none; type is null when the result has no type property. The bytes field is the on-disk file size. With include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.` @@ -70,13 +87,14 @@ Example: vault_search({ query: "decision", filters: { modified: { after: "2026-0 } : { stage: "matching", - outcome: "a query left with no terms returns { results: [], total: 0 }", + outcome: "a query left with no terms returns no results", } + const searchLagLine = + "Behavior: Results can lag a file change by a few seconds, so a note written moments ago may not be found yet." const searchErrorsSection = `Errors: -- No matches returns { results: [], total: 0 }, not an error +- No matches returns an empty results array with total 0, not an error - Malformed query syntax never throws: the words AND, OR, NOT and NEAR in any letter case, and punctuation such as *, ^, parentheses and colons, are dropped before ${emptiedQueryText.stage}; ${emptiedQueryText.outcome} -- "invalid created.on date: …" (any created/modified bound) — the bound isn't in YYYY-MM-DD form; use that form -- "… is not a calendar date" — a created/modified bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date +- "invalid created.on date: …" or "… is not a calendar date" (any created/modified bound) — pass a real calendar date in YYYY-MM-DD form - "tag must not be empty …" — a tags entry is only "#"; pass the tag name ${INDEX_ERROR_ENTRY}` @@ -92,8 +110,9 @@ ${searchFiltersSection} Example: vault_search({ query: "how the server watches for file changes" }) ${searchSharedExamples} -When to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords. -${searchAlternativesLine} +When to use: The primary discovery tool for content-based queries, optionally constrained by metadata.${searchAlternativesLine} + +${searchLagLine} ${searchErrorsSection} @@ -105,8 +124,9 @@ ${searchFiltersSection} ${searchSharedExamples} Example: vault_search({ query: "deployment", filters: { properties: { status: "active" } } }) -When to use: The primary discovery tool for content-based queries, optionally constrained by metadata. -${searchAlternativesLine} +When to use: The primary discovery tool for content-based queries, optionally constrained by metadata.${searchAlternativesLine} + +${searchLagLine} ${searchErrorsSection} @@ -209,7 +229,7 @@ Example: vault_search_by_tag({ tag: "project", exact: true, limit: 50 }) — not When to use: Tag-only lookups, for one tag or a whole tag hierarchy, with no text query.${whenToolEnabledText("vault_list_tags", " Use vault_list_tags first to discover available tags.")}${whenToolEnabledText("vault_search", "\nPrefer vault_search when you need text-based relevance ranking.")} Parameters: -- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: a result count equal to limit may mean more exist, so raise limit to check. +- limit applies after sorting, so you get the most recently modified notes; nothing in the response signals truncation. Behavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, and one moved, deleted, or retagged moments ago may still be listed. @@ -219,14 +239,12 @@ Errors: - "tag must not be empty …" — tag is "#" alone; pass the tag name. ${INDEX_ERROR_ENTRY} -Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (every frontmatter tag Obsidian accepts, in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, +Returns: JSON array of notes sorted by most recently modified, then by path ascending. Each note has path, folder (top-level folder; "" at the vault root), bytes (on-disk size), modified (file modification time), and the frontmatter values title (file name without .md when missing or not text), tags (in the note's letter case), related ([] when missing), type (null when missing or not text), and created (null when missing or not an ISO date). Timestamps are ISO 8601. When present, a note also has leading_callout (the callout opening its body, as { type, title, body }) and additional_properties (every other frontmatter key).`, inputSchema: { tag: z .string() .min(1) - .describe( - 'Tag, "#" optional (e.g. "project", "session-log"). Nested tags use "/" (e.g. "project/vault-cortex").', - ), + .describe('Tag, "#" optional (e.g. "project", "session-log", "project/vault-cortex")'), exact: z .boolean() .optional() @@ -258,6 +276,17 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce }, ) + const listTagsAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_search_by_tag", + "Prefer vault_search_by_tag once you know which tag to query.", + ), + whenToolEnabledText( + "vault_list_property_values", + 'To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: "tags" }) with folder or limit.', + ), + ]) + registerTool( TOOL_NAMES.VAULT_LIST_TAGS, { @@ -266,8 +295,7 @@ Returns: JSON array of notes sorted by most recently modified, then by path asce Example: vault_list_tags() returns [{ tag: "session-log", count: 42 }, { tag: "project/vault-cortex", count: 8 }, ...] -When to use: Discovering what tags exist before searching by tag. Good first step for vault orientation. -Prefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching ("project" matches "project/*"). +When to use: Discovering what tags exist before searching by tag. Good first step for vault orientation.${listTagsAlternativesLine} Errors: - A vault with no tagged notes returns an empty array, not an error. @@ -293,6 +321,17 @@ Returns: JSON array of { tag, count }; tag omits the "#" prefix.`, }, ) + const recentNotesAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_search", + "Prefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters).", + ), + whenToolEnabledText( + "vault_search_by_folder", + "Prefer vault_search_by_folder for browsing a specific folder.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_RECENT_NOTES, { @@ -302,13 +341,14 @@ Returns: JSON array of { tag, count }; tag omits the "#" prefix.`, Example: vault_recent_notes({ sort_by: "modified", limit: 10 }) Example: vault_recent_notes({ sort_by: "created", limit: 5 }) -When to use: Catching up on vault changes, finding recent work, or orienting after a break. -Prefer vault_search for content-based discovery, including a text query limited to a date range (its created and modified filters). Prefer vault_search_by_folder for browsing a specific folder. +When to use: Catching up on vault changes, finding recent work, or orienting after a break.${recentNotesAlternativesLine} Parameters: - sort_by + limit interact: "modified" uses filesystem mtime, which every note has. "created" uses the frontmatter created property; notes without a valid one sort after every dated note, so a small limit can leave them out — use "modified" to see them. - sort_by "modified" includes any file write (content edits, property changes, sync touches), so recently-synced notes appear recent even without user edits. +Behavior: A file change takes a few seconds to appear here, so a note written moments ago may not be listed yet. + Errors: - An empty vault returns an empty array, not an error. ${INDEX_ERROR_ENTRY} @@ -319,7 +359,7 @@ Returns: JSON array of note metadata (path, title, tags, related, folder (top-le .enum(["created", "modified"]) .optional() .default("modified") - .describe('Timestamp to sort by, newest first (default "modified")'), + .describe('Timestamp to sort by (default "modified")'), limit: z .number() .int() @@ -346,6 +386,18 @@ Returns: JSON array of note metadata (path, title, tags, related, folder (top-le }, ) + const folderLinkTools = formatEnabledToolList([ + TOOL_NAMES.VAULT_GET_BACKLINKS, + TOOL_NAMES.VAULT_GET_OUTGOING_LINKS, + ]) + const searchByFolderAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText("vault_list_notes", "Prefer vault_list_notes when you only need paths."), + whenToolEnabledText("vault_search", "Prefer vault_search when you have a text query."), + folderLinkTools + ? `Use ${folderLinkTools} to explore how notes in a folder connect to the rest of the vault.` + : "", + ]) + registerTool( TOOL_NAMES.VAULT_SEARCH_BY_FOLDER, { @@ -355,12 +407,11 @@ Returns: JSON array of note metadata (path, title, tags, related, folder (top-le Example: vault_search_by_folder({ folder: "Projects" }) Example: vault_search_by_folder({ folder: "${config.memoryEnabled ? config.memoryDir : "Projects/Archive"}", recursive: false, limit: 10 }) — the folder's own notes, 10 most recent -When to use: Exploring a folder's contents with full context for vault orientation. -Prefer vault_list_notes when you only need paths. Prefer vault_search when you have a text query. Use vault_get_backlinks or vault_get_outgoing_links to explore how notes in a folder connect to the rest of the vault. +When to use: Exploring a folder's contents with full context for vault orientation.${searchByFolderAlternativesLine} Parameters: - folder names a whole folder, not a text prefix: "Projects" matches notes under "Projects/" but not "ProjectsOld/". Matching ignores ASCII letter case, and a trailing slash is ignored. -- limit applies after sorting, so you get the most recently modified notes. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check. +- limit applies after sorting, so you get the most recently modified notes. There is no offset, and nothing in the response signals truncation; raise limit to see more. Behavior: Results can lag a file change by a few seconds: a note written moments ago may not appear yet, one renamed or deleted moments ago may still be listed, and a just-changed note may show its old metadata. Notes in hidden (dot-prefixed) folders never appear. @@ -368,7 +419,7 @@ Errors: - An empty or nonexistent folder returns an empty array, not an error. ${INDEX_ERROR_ENTRY} -Returns: JSON array of note metadata sorted by most recently modified, then by path: path, title, tags, related, folder (top-level folder), type, created (frontmatter; null when missing), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).`, +Returns: JSON array of note metadata sorted by most recently modified, then by path: path, title (file name without .md when missing or not text), tags, related ([] when missing), folder (top-level folder), type (null when missing or not text), created (frontmatter; null when missing or not an ISO date), modified (file time), bytes (on-disk size), plus, when present, leading_callout (the note's opening callout, { type, title, body }) and additional_properties (other frontmatter keys).`, inputSchema: { folder: z .string() @@ -380,7 +431,7 @@ Returns: JSON array of note metadata sorted by most recently modified, then by p .boolean() .optional() .default(true) - .describe("Include subfolders (default: true); false lists only the folder's top level"), + .describe("Include subfolders (default: true)"), limit: z.number().int().min(1).optional().default(20).describe("Max results (default 20)"), }, }, @@ -401,17 +452,27 @@ Returns: JSON array of note metadata sorted by most recently modified, then by p }, ) + const listPropertyKeysAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_list_property_values", + "Prefer vault_list_property_values when you need the full list of values for a specific key.", + ), + whenToolEnabledText( + "vault_search_by_property", + "Prefer vault_search_by_property to find notes matching a specific key-value pair.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_LIST_PROPERTY_KEYS, { title: "List Property Keys", - description: `Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes. + description: `Discover all property keys in the vault with note counts and sample values, without reading individual notes. Example: vault_list_property_keys() returns [{ key: "tags", count: 342, sample_values: ["session-log", "project"] }, ...] Example: vault_list_property_keys({ folder: "Projects" }) — the keys used under Projects/ only -When to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags. -Prefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair. +When to use: Discovering what properties exist before searching by property. Good first step for vault orientation${whenToolEnabledText("vault_list_tags", " alongside vault_list_tags")}.${listPropertyKeysAlternativesLine} Parameters: - folder names a whole folder and includes its subfolders: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Matching ignores ASCII letter case; omit folder to scan the entire vault. @@ -459,12 +520,12 @@ Example: vault_list_property_values({ key: "tags", limit: 10 }) — the 10 most- When to use: Enumerating possible values for a property key${whenToolEnabledText("vault_search_by_property", " before calling vault_search_by_property")}.${whenToolEnabledText("vault_list_property_keys", " Call vault_list_property_keys first to discover valid key names.")}${whenToolEnabledText("vault_search_by_property", ` vault_search_by_property matches stored numbers numerically and text exactly; value "1" matches the number 1, the text "1", and a checked checkbox.`)} Parameters: -- key is case-sensitive and must match exactly as returned by vault_list_property_keys. +- key is case-sensitive and must match exactly${whenToolEnabledText("vault_list_property_keys", " as returned by vault_list_property_keys")}. - folder names a whole folder and includes its subfolders: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Matching ignores ASCII letter case. -- limit applies after sorting by count descending, so you always get the most-used values first. Nothing in the response signals truncation: exactly limit values may mean more exist, which is common for keys with many distinct values like "title" or "created"; raise limit to check. +- limit applies after sorting by count descending, so you get the most-used values first; nothing in the response signals truncation. Behavior: -- Handles both scalar properties (status: "active") and array properties (tags: ["a", "b"]). Array elements are unpacked and counted individually, so the sum of counts may exceed the note count. +- Array elements (tags: ["a", "b"]) are counted individually, so the sum of counts may exceed the note count. - Values with the same displayed string share one row with combined occurrence counts: number 1 and text "1" count together, while text "1.0" stays separate. Grouping happens before limit. - Checkbox true and false come back as "1" and "0", counted with the numbers 1 and 0. - null values are skipped. @@ -503,6 +564,34 @@ Returns: JSON array of { value, count } sorted by count descending, then by valu }, ) + // The two discovery tools share one "Use … and …" sentence, so each clause + // joins it only while its tool is served. + const propertyDiscoveryTools = [ + whenToolEnabledText( + "vault_list_property_keys", + "vault_list_property_keys to discover valid keys", + ), + whenToolEnabledText( + "vault_list_property_values", + "vault_list_property_values to see what values a key takes", + ), + ] + .filter(Boolean) + .join(" and ") + const propertyDiscoverySentence = propertyDiscoveryTools ? `Use ${propertyDiscoveryTools}.` : "" + + const searchByPropertyAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_search", + "Prefer vault_search when you also have a text query (it supports property filters too).", + ), + whenToolEnabledText( + "vault_search_by_tag", + "Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching).", + ), + propertyDiscoverySentence, + ]) + registerTool( TOOL_NAMES.VAULT_SEARCH_BY_PROPERTY, { @@ -512,24 +601,26 @@ Returns: JSON array of { value, count } sorted by count descending, then by valu Example: vault_search_by_property({ key: "status", value: "in-progress" }) Example: vault_search_by_property({ key: "type", value: "session-log", folder: "Code Projects" }) -When to use: Finding notes by metadata when you don't have a text query. -Prefer vault_search when you also have a text query (it supports property filters too). Prefer vault_search_by_tag for tag-specific queries (supports hierarchical prefix matching). Use vault_list_property_keys to discover valid keys and vault_list_property_values to see what values a key takes. +When to use: Finding notes by metadata when you don't have a text query.${searchByPropertyAlternativesLine} Parameters: - key matches exactly and case-sensitively; value matches stored text the same way, with no partial matching or globbing. -- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: "04" and "4.0" match number 4 and their own literal text, but not text "4". Anything else ("4abc", " 4", 0b binary, "1e999") matches only as text. Numbers compare as 64-bit floats, so integers past 2^53 can compare equal and "1e-999" matches 0. -- Pass a checkbox as "1" (checked) or "0" (unchecked); a checkbox matches only as text, so "1.0" and "true" do not match. -- An array element must equal value in full: "blog" matches tags: ["blog", "draft"] but not tags: ["my-blog"]. +- A value written as a complete, finite YAML number (-4, 04, .5, 4., 1e3, 0x1F, 0o17) also matches stored numbers numerically: "04" and "4.0" match number 4 and their own literal text, but not text "4". Anything else ("4abc", " 4", 0b binary) matches only as text. +- value for a checkbox: pass "1" (checked) or "0" (unchecked); a checkbox matches only as text, so "1.0" and "true" do not match. +- value against a list property: an element must equal value in full: "blog" matches tags: ["blog", "draft"] but not tags: ["my-blog"]. - folder names a whole folder and includes its subfolders: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Matching ignores ASCII letter case; omit folder to search the entire vault. -- limit keeps the most recently modified matches. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check. +- limit keeps the most recently modified matches; nothing in the response signals truncation. + +Behavior: Results can lag a file change by a few seconds: a note written moments ago may not match yet, and one changed moments ago may still match its old value. Errors: - An unknown key or unmatched value returns an empty array, not an error. ${INDEX_ERROR_ENTRY} -Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending — recently-synced notes may sort ahead of older content edits. +Returns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties?), sorted by filesystem mtime descending, then by path — recently-synced notes may sort ahead of older content edits. +- folder is the top-level folder ("" at the vault root); title falls back to the file name without .md when missing or not text; related is [] when missing; type is null when missing or not text; created is null when missing or not an ISO date. - leading_callout appears only when the note has a leading callout. -- additional_properties appears only when frontmatter has keys outside title, type, created, related, and the first key spelled tags in any letter case.`, +- additional_properties appears only when frontmatter has keys outside title, type, created, related, and tags.`, inputSchema: { key: z.string().min(1).describe('Property key name (e.g. "status", "type", "tags").'), value: z.string().min(1).describe('Value to match (e.g. "active", "4", "1e-7").'), @@ -560,17 +651,29 @@ Returns: JSON array of note metadata (path, title, tags, related, folder, type, }, ) + // Both link tools return an empty result for a path the index does not hold. // Every path-finding tool can be dropped via DISABLED_TOOLS, so the remedy // falls back to checking the path itself when none is served. - const backlinksPathFinders = formatEnabledToolList([ + const linkPathFinders = formatEnabledToolList([ TOOL_NAMES.VAULT_SEARCH, TOOL_NAMES.VAULT_LIST_NOTES, TOOL_NAMES.VAULT_LIST_FILES, ]) - const backlinksEmptyResultRemedy = backlinksPathFinders - ? `find valid paths with ${backlinksPathFinders}` + const linkEmptyResultRemedy = linkPathFinders + ? `find valid paths with ${linkPathFinders}` : "check the path's spelling and letter case" + const backlinksAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_get_outgoing_links", + "For outgoing links (what a note links TO), use vault_get_outgoing_links.", + ), + whenToolEnabledText( + "vault_find_orphans", + "To find notes nothing else links to, use vault_find_orphans.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_GET_BACKLINKS, { @@ -580,14 +683,16 @@ Returns: JSON array of note metadata (path, title, tags, related, folder, type, Example: vault_get_backlinks({ path: "Projects/vault-cortex.md" }) Example: vault_get_backlinks({ path: "Diagrams/architecture.canvas" }) -When to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph. -For outgoing links (what a note links TO), use vault_get_outgoing_links. To find notes nothing else links to, use vault_find_orphans. +When to use: Understanding what references a note or canvas, assessing its connectivity before editing or deleting, or finding related notes via the graph.${backlinksAlternativesLine} + +Parameters: +- path: pass the target's own path, never a link's text: a link written as [[Note]], as a relative ../Note.md, or as a full path counts toward the note it resolves to. Returns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title; bytes is the source's on-disk size), and count. Errors: -- "path must end in …" — add the .md or .canvas extension -- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, ${backlinksEmptyResultRemedy}. +${LINK_GRAPH_EXTENSION_ERROR_ENTRY} +- A path nothing links to returns an empty result (count 0), not an error; links from a note written or changed in the last few seconds may not appear yet. If you expected links, ${linkEmptyResultRemedy}. ${INDEX_ENGINE_ERROR_ENTRY}`, inputSchema: { path: z @@ -620,23 +725,32 @@ ${INDEX_ENGINE_ERROR_ENTRY}`, "vault_read_file", " — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image is still cheap to read", ) + const outgoingLinksAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_get_backlinks", + "For incoming links (what links TO a note), use vault_get_backlinks.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_GET_OUTGOING_LINKS, { title: "Get Outgoing Links", - description: `Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes (vault-relative paths embedded in the canvas), not links inside text cards. Links inside code blocks are ignored; self-links are included. Each existing target is listed once, however often the note links to it. A broken link's path is its target text up to any #heading or |alias, in its original letter case, with a markdown link's %-escapes decoded; each distinct path is listed once. + description: `Find all notes and files a given note or canvas links to. For notes: [[wikilinks]], [markdown](links), ![[embeds]], and wikilinks inside frontmatter properties (e.g. related:). For canvas files: file nodes, not links inside text cards. Links inside code blocks are ignored; self-links are included. Each target is listed once, however often the note links to it. A broken link's path is its target text without any #heading or |alias, with a markdown link's %-escapes decoded. Example: vault_get_outgoing_links({ path: "Projects/vault-cortex.md" }) Example: vault_get_outgoing_links({ path: "Diagrams/architecture.canvas" }) -When to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing. -For incoming links (what links TO a note), use vault_get_backlinks. +When to use: Navigating the graph forward, auditing broken links in one note or canvas, or checking dependencies before editing.${outgoingLinksAlternativesLine} + +Parameters: +- path: a note or canvas created or moved in the last few seconds is not found yet. -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken, and kind is "note" even for a path like "diagram.png". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — expected "create on click" navigation to a daily note that doesn't exist yet, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links)${fileBytesClause}. +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. When exists is true: kind "note" is a markdown note${whenToolEnabledText("vault_read_note", " readable via vault_read_note")}; kind "file" is a non-markdown file (.canvas, image, PDF)${fileReadableClause}. When exists is false the link is broken, and kind is "note" even for a path like "diagram.png". The title field is null for files and broken links. The daily_note_forward_ref flag is true on a broken link into the vault's daily notes folder — a daily note not yet created, not genuine breakage — and false otherwise, including when the daily notes folder is the vault root or blank. The bytes field is the on-disk size of a note or file (null for broken links)${fileBytesClause}. Errors: -- "path must end in …" — add the .md or .canvas extension -- A path that does not exist, a note or canvas in a dot-prefixed folder, or one created or moved in the last few seconds returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links. +${LINK_GRAPH_EXTENSION_ERROR_ENTRY} +- A path that does not exist, or one in a dot-prefixed folder, returns an empty result (count 0), not an error, just as a note with no outbound links does. If you expected links, ${linkEmptyResultRemedy}. ${INDEX_ENGINE_ERROR_ENTRY}`, inputSchema: { path: z @@ -690,32 +804,40 @@ ${INDEX_ENGINE_ERROR_ENTRY}`, : { folders: `the daily notes folder, "Templates", ${JSON.stringify(config.memoryDir)}`, dailyFolderLine: isToolEnabled("vault_get_daily_note") - ? " The defaults' daily notes folder is the one vault_get_daily_note's path starts with; when it is the vault root or blank, the defaults hold no daily folder." - : " When the vault's daily notes folder is the vault root or blank, the defaults hold no daily folder.", + ? " The defaults' daily notes folder is the one vault_get_daily_note's path starts with." + : "", } + const findOrphansAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_get_backlinks", + "Prefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_FIND_ORPHANS, { title: "Find Orphans", - description: `Find notes with no incoming links from other notes or canvases — orphans are disconnected from the knowledge graph and may be forgotten or need linking. A note that only links to itself still counts as an orphan. + description: `Find notes with no incoming links from other notes or canvases. A note that only links to itself still counts as an orphan. Example: vault_find_orphans({}) Example: vault_find_orphans({ exclude_folders: ["Templates", "Archive"], limit: 20 }) -When to use: Vault maintenance — surfacing notes to integrate into the graph.${whenToolEnabledText("vault_patch_note", " Link an orphan by mentioning it from a relevant note with vault_patch_note.")} -Prefer vault_get_backlinks to check the connectivity of one specific note rather than scanning the whole vault. +When to use: Vault maintenance — surfacing notes to integrate into the graph.${whenToolEnabledText("vault_patch_note", " Link an orphan by mentioning it from a relevant note with vault_patch_note.")}${findOrphansAlternativesLine} Parameters: - exclude_folders replaces the defaults, it does not add to them — list a default yourself to keep it. Pass [] for no exclusions. Each entry names a whole folder, subfolders included ("Projects" also excludes "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. Excluded notes are left out of the results; links from them still count as incoming links.${orphanDefaultsText.dailyFolderLine} -- limit applies after exclusions and sorting by most recently modified. Nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to check. +- limit applies after exclusions and sorting; nothing in the response signals truncation. + +Behavior: A file change takes a few seconds to appear here, so a link written moments ago may not count yet. Errors: - An empty array means no orphans were found (after exclusions), not an error. -- "too many excluded folders" — pass a shorter exclude_folders list, then retry. +- "too many excluded folders" — pass at most 994 exclude_folders entries, then retry. ${INDEX_ERROR_ENTRY} -Returns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created, modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.`, +Returns: JSON array of note metadata (path, title, tags, related, folder (top-level folder), type, created (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified. The bytes field is the on-disk file size. A leading_callout field appears only when the note has one; additional_properties only when frontmatter has keys outside title, type, created, related, and tags.`, inputSchema: { exclude_folders: z .array(z.string().min(1)) diff --git a/src/vault-mcp/mcp-core/tools/task-tools.ts b/src/vault-mcp/mcp-core/tools/task-tools.ts index b0cd53650..a0f66cebe 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -4,38 +4,64 @@ import { z } from "zod" import { TOOL_NAMES } from "../tool-registry.js" import type { ToolRegistrationContext } from "./tool-helpers.js" import { + CONCURRENT_WRITE_RETRY_ERROR_ENTRY, dateFilterSchema, FILESYSTEM_ERROR_ENTRY, INDEX_ERROR_ENTRY, describePropertiesBlockErrorEntry, + formatServedSentencesLine, + NOTE_EXTENSION_ERROR_ENTRY, OPENING_BLOCK_ERROR_ENTRY, + PATH_SAFETY_ERROR_ENTRY, } from "./tool-helpers.js" import { taskMutations } from "../../vault-operations/task-mutations.js" +// Errors entries vault_create_task and vault_update_task share word for word. +const TASK_HEADING_NOT_FOUND_ERROR_ENTRY = + '- "heading "X" not found; available: ..." — the message lists the note\'s headings' +const TASK_ID_CHARACTERS_ERROR_ENTRY = + '- "taskId ... contains invalid characters" / "dependsOn entry ... contains invalid characters" — the message names the allowed characters' +const RECURRENCE_RULE_ERROR_ENTRY = + '- "unrecognized recurrence rule ..." — the message gives example rules' +const TASK_DATE_ERROR_ENTRY = '- "invalid date: …" — the message names the expected format' + export const registerTaskTools = ({ registerTool, safeHandler, + isToolEnabled, whenToolEnabledText, vaultPath, search, logger: sessionLogger, }: ToolRegistrationContext): void => { + // The note-not-found Errors entry of both task write tools. + const noteNotFoundErrorEntry = `- "note not found" — path does not exist; check the path${whenToolEnabledText("vault_list_notes", " with vault_list_notes")}` + // ── vault_list_tasks ──────────────────────────────────────────── + const listTasksAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_read_note", + "Prefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write.", + ), + whenToolEnabledText( + "vault_search", + "Prefer vault_search for full-text queries over note content.", + ), + ]) + registerTool( TOOL_NAMES.VAULT_LIST_TASKS, { title: "List Tasks", - description: `List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏳ scheduled, 🛫 start, ➕ created, ✅ done, ❌ cancelled, 🔺⏫🔼🔽⏬ priority, 🔁 recurrence, 🏁 onCompletion, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result says where the task lives (path, line, heading; see Returns), so no follow-up reads are needed to locate it. Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status registry are also excluded. + description: `List checkbox tasks across the whole vault with structured filters — the Tasks-plugin data model over MCP. Both task metadata formats are indexed: emoji signifiers (📅 due, ⏫ priority, 🔁 recurrence, ...) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Task lines inside fenced code blocks and %% %% comment blocks are not indexed. Checkboxes typed NON_TASK in the Tasks plugin's status settings are also excluded. Example: vault_list_tasks({ due: { before: "2026-07-04" } }) — overdue triage; the default status (not_done) and sort (due ascending) make this the "what's overdue?" call Example: vault_list_tasks({ path: "Code Projects/vault-cortex/TASKS.md", heading: ["Active", "Up Next", "Waiting On"], sort_by: "position" }) — actionable Kanban lanes in board order Example: vault_list_tasks({ folder: "Code Projects/vault-cortex" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories) Example: vault_list_tasks({ status: "done", done: { after: "2026-06-26" } }) — what got completed this week -Example: vault_list_tasks({ top_level_only: true, path: "TASKS.md" }) — board cards only, excluding checklist sub-items -When to use: Reading task status and order on one board (path + sort_by: "position"; add status: "all" to include done and cancelled cards). Also any vault-wide task triage question — "what's overdue?", "what's open per project?", "what did I finish this week?" — in one call instead of per-board reads. -${whenToolEnabledText("vault_read_note", "Prefer vault_read_note (heading mode) only when you need a lane's verbatim Markdown or a task's state right after a write. ")}Prefer vault_search for full-text queries over note content. +When to use: Reading task status and order on one board (path + sort_by: "position"; add status: "all" to include done and cancelled cards). Also any vault-wide task triage question — "what's overdue?", "what's open per project?", "what did I finish this week?" — in one call instead of per-board reads.${listTasksAlternativesLine} Behavior: A file change takes a few seconds to appear here, so a task written moments ago may still show its old state. @@ -43,16 +69,17 @@ Parameters: - status: "not_done" and "all" expand in arrays — ["not_done", "done"] matches todo + in_progress + done. - due / scheduled / start / done / created / cancelled: a date filter only matches tasks that HAVE that date. - folder: a whole folder, subfolders included ("Projects" covers "Projects/Archive" but not "ProjectsOld/"), ignoring ASCII letter case. +- sort_by: a task with none of a date sort's dates comes last; "priority" ascending puts highest first, and tasks with no priority sort between medium and low. Errors: - "invalid due.before date: …" (any date filter's bound) — the bound isn't in YYYY-MM-DD form; use that form -- "… is not a calendar date" — the bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date -- "path must end in .md" — add the ".md" extension +- "… is not a calendar date" — a date filter's bound has the YYYY-MM-DD form but names a day that doesn't exist, such as 2026-02-30; pass a real date +${NOTE_EXTENSION_ERROR_ENTRY} - "tag must not be empty …" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error ${INDEX_ERROR_ENTRY} -Returns: JSON { total, tasks }. Every task carries path, line, status, status_char, description, folder, depth (0 for top-level, 1+ for sub-tasks), is_kanban_task, depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only), and subtask_progress — { done, total } over the task's DIRECT checklist children, present only when the task has a checklist; done counts status "done" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.`, +Returns: JSON { total, tasks }. Every task carries path, line, status, status_char (the checkbox character, such as "x" or "/"), description, folder (the task note's parent folder), depth (0 for top-level, 1+ for sub-tasks), is_kanban_task (true when the task's note is a Kanban board, with kanban-plugin in its properties), depends_on, and tags (the arrays are [] when empty). Every other field appears only when the task has it: heading (nearest heading above the task), created/scheduled/start/due/done/cancelled dates, priority, recurrence, on_completion, task_id, block_id, parent_block_id (sub-tasks whose parent carries a ^block-id), done_lanes (Kanban boards only: the headings of the lanes whose first line is **Complete**), and subtask_progress — { done, total } over the task's DIRECT checklist children; done counts status "done" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.`, inputSchema: { status: z .union([ @@ -61,9 +88,7 @@ Returns: JSON { total, tasks }. Every task carries path, line, status, status_ch ]) .optional() .default("not_done") - .describe( - 'Status filter, OR-combined (default "not_done" = todo + in_progress, excluding done and cancelled). "all" includes every status.', - ), + .describe('Status filter, OR-combined (default "not_done" = todo + in_progress).'), due: dateFilterSchema.describe("Due date (📅 / [due:: ]) bounds"), scheduled: dateFilterSchema.describe("Scheduled date (⏳ / [scheduled:: ]) bounds"), start: dateFilterSchema.describe("Start date (🛫 / [start:: ]) bounds"), @@ -126,13 +151,13 @@ Returns: JSON { total, tasks }. Every task carries path, line, status, status_ch .optional() .default("due") .describe( - 'Sort key (default "due"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade); each fallback uses its own natural direction. "position" sorts by file path then line number — the natural order for Kanban boards.', + 'Sort key (default "due"). Date sorts cascade through related fields when the primary is absent (through the rest of due → scheduled → start → created, in that order; done does not cascade). "position" sorts by file path then line number.', ), sort_direction: z .enum(["asc", "desc"]) .optional() .describe( - 'Sort direction. Default per field: "asc" for due/scheduled/priority/position, "desc" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.', + 'Sort direction. Default per field: "asc" for due/scheduled/priority/position, "desc" for start/created/done/note_mtime. When a date sort cascades (see sort_by), each field uses its own default direction; an explicit value overrides all fields uniformly.', ), }, }, @@ -221,7 +246,7 @@ Returns: JSON { total, tasks }. Every task carries path, line, status, status_ch TOOL_NAMES.VAULT_CREATE_TASK, { title: "Create Task", - description: `Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo (the Tasks plugin's todo symbol, [ ] by default), with its created date (➕) set to today${whenToolEnabledText("vault_update_task", "; set in_progress later with vault_update_task")}. Override format only at the user's request, since the Tasks plugin reads just one format. + description: `Create a correctly-formatted task in one call — description, target heading, dates, priority, block_id, and optional checklist sub-items. The task is always created as todo, with its created date (➕) set to today${whenToolEnabledText("vault_update_task", "; set in_progress later with vault_update_task")}. Override format only at the user's request. Example: vault_create_task({ path: "TASKS.md", description: "Fix login bug", block_id: "fix-login", heading: "Active", priority: "high", due: "2026-09-15" }) — a card in a Kanban board's Active lane Example: vault_create_task({ path: "TASKS.md", description: "Sub-bug", parent_block_id: "fix-login", due: "2026-09-01" }) — full sub-task under fix-login @@ -233,38 +258,36 @@ When to use: Creating a new task card on a board or in a note. Guarantees correc Parameters: - heading is required on Kanban boards (notes with a kanban-plugin property) unless a parent is given. - parent_block_id / parent_line: pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives. -- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. When the Kanban plugin saves the board, it copies an indented line's block ID onto its card, replacing the card's own ID. +- block_id / subtasks: block_id is required, except on a Kanban sub-task, where it is refused, as is a subtasks item ending in a ^block-id on a board. - position: ignored without a heading, and under a parent, where the sub-task goes after the parent's last sub-item. -- priority: the Tasks plugin ranks "no signifier" (normal priority) between medium and low. - recurrence: a rule ending "when done" bases the next occurrence on the completion day; a task with no dates recurs as a dateless copy. -- due / scheduled / start: omit a date rather than guessing. Errors: (messages use camelCase names, e.g. blockId for block_id) -- "note not found" — path does not exist; check the path${whenToolEnabledText("vault_list_notes", " with vault_list_notes")} -- "path must end in …" — add the .md extension -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "heading required for Kanban boards" — kanban-plugin note without heading; pass heading with the target lane -- "heading "X" not found; available: ..." — no heading matches; the error lists the note's headings +${noteNotFoundErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +- "heading required for Kanban boards" — pass heading with the target lane +${TASK_HEADING_NOT_FOUND_ERROR_ENTRY} - "cannot place at position P under "X" — the heading appears N times" — integer position on a note with duplicate heading names; use "top" or "bottom" (they place under the first match), or rename one section to make it unique -- "parent task not found" — parent_block_id or parent_line doesn't resolve to a task, or the line is inside a fenced code block or %% %% comment; re-read the parent's block_id or line${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")} +- "parent task not found" — parent_block_id or parent_line doesn't resolve to a task outside a fenced code block or %% %% comment; re-read the parent's block_id or line${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")} - "parent task ambiguous" — more than one task line ends in that ^id (the message lists their line numbers); pass parent_line instead - "checkbox "[c]" is a NON_TASK status" / "no checkbox symbol for status ..." — in the Tasks plugin's status settings, the parent's checkbox character (c) is typed NON_TASK so it is not a task, or todo has no symbol and the default [ ] character has another type; the user must change that character's type or add a todo symbol there, then restart the server - "parentBlockId and parentLine are mutually exclusive" / "parent and heading are mutually exclusive" — drop one -- "blockId ... already exists in this note" / "blockId ... contains invalid characters" — block_id must be unique in the note and match [a-zA-Z0-9-]+ +- "blockId ... already exists in this note" / "blockId ... contains invalid characters" — pick a block_id no other line in the note ends in; the invalid-characters message names the allowed characters - "blockId is required" — pass block_id -- "blockId is not allowed on a sub-task on a Kanban board" / "subtask ... ends in a block ID" — drop the block_id, or the trailing ^id from the checklist item +- "blockId is not allowed on a sub-task on a Kanban board" / "subtask ... ends in a block ID" — the message names what to drop - "description is empty" / "subtasks cannot contain an empty item" / "description must be a single line" / "subtasks items must be a single line" — pass visible text on a single line -- "taskId ... contains invalid characters" / "dependsOn entry ... contains invalid characters" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ -- "unrecognized recurrence rule ..." — the rule text is not Tasks-plugin natural language; use a rule such as "every week" or "every 2 weeks when done" -- "invalid date: …" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date -- "concurrent write in progress" — another write to this note is in flight; retry +${TASK_ID_CHARACTERS_ERROR_ENTRY} +${RECURRENCE_RULE_ERROR_ENTRY} +${TASK_DATE_ERROR_ENTRY} +${CONCURRENT_WRITE_RETRY_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${describePropertiesBlockErrorEntry()} ${OPENING_BLOCK_ERROR_ENTRY} -Obsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like "🔁 every week", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. +Obsidian syntax: When description or subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like "🔁 every week", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. -Returns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; description is the text as submitted; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none) — checklist items are written without a block_id, so line is the handle for a follow-up update; changes lists every field written as "field: (none) → value", and checklist items as "subtasks: 0 → N"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.`, +Returns: JSON { path, line, description, block_id, heading, subtasks, changes, advisories } — line is the new task's 1-based line number in the file; block_id is omitted when the task has none (line is then its handle); heading is the nearest heading above the new task (omitted when the note has none); subtasks lists each checklist item written as { line, description } (omitted when none); changes lists every field written as "field: (none) → value", and checklist items as "subtasks: 0 → N"; advisories (omitted when the line round-trips clean) lists one sentence per place the stored line parses back differently than submitted — see Obsidian syntax above.`, inputSchema: { path: z .string() @@ -354,7 +377,7 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, changes, a .min(1) .optional() .describe( - "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Non-empty; omit when there are no dependencies.", + "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks), not checked against existing tasks. Omit when there are no dependencies.", ), subtasks: z .array(z.string().min(1)) @@ -459,6 +482,10 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, changes, a // ── vault_update_task ─────────────────────────────────────────── + const updateTaskLineDescription = isToolEnabled("vault_list_tasks") + ? "1-based line number from vault_list_tasks. Fragile if the file changed since the query." + : "1-based line number of the task. Fragile if the note changed since you read it." + registerTool( TOOL_NAMES.VAULT_UPDATE_TASK, { @@ -471,55 +498,54 @@ Example: vault_update_task({ path: "TASKS.md", block_id: "my-task", heading: "Do Example: vault_update_task({ path: "TASKS.md", block_id: "my-task", description: "Updated task name", due: "2026-10-01", scheduled: null }) — change the description, set one date, and clear another Example: vault_update_task({ path: "TASKS.md", block_id: "my-task", status: "in_progress", add_subtasks: ["Design", "Implement", "Test"] }) — start working and add checklist stages Example: vault_update_task({ path: "TASKS.md", line: 42, assign_block_id: "my-task" }) — add a block_id to a task that lacks one -Example: vault_update_task({ path: "TASKS.md", block_id: "my-task", heading: "Active", position: 3 }) — move to the 3rd position in a lane When to use: Any change to an existing task.${whenToolEnabledText("vault_list_tasks", " Use vault_list_tasks first to get identification fields (path + block_id or line).")}${whenToolEnabledText("vault_create_task", " For creating a new task, use vault_create_task instead.")} Parameters: -- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")} just before an update by line: each save by the Kanban plugin rewrites the board${whenToolEnabledText("vault_list_tasks", ". The listing lags each save by a few seconds, so wait that long after a write before re-reading")}. +- Exactly one of block_id or line is required to identify the task. A Kanban sub-task this server created has no block_id, so re-read its line${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")} just before an update by line.${whenToolEnabledText("vault_list_tasks", " After a write, wait a few seconds for the listing to catch up.")} - At least one change is required. Clearing is always explicit null — omitting a field leaves it untouched. - status side effects: - Kanban: "done" moves the card and everything nested under it to the done lane (the lane holding a **Complete** line, else the heading "Done", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent. + Kanban: "done" moves the card and everything nested under it to the done lane (the lane whose first line is **Complete**, else the heading "Done", exact case), leaving nested checkboxes as they are; a sub-task stays under its parent. Recurring (🔁), on "done" only: spawns the next occurrence, dates advanced per the rule, directly above the completed task, which keeps its nested lines. When the Tasks plugin's settings put the next recurrence on the line below, the spawn goes on the line directly below the task instead, so the task's nested lines then sit under the spawn. The spawn stays in the source lane with no block_id, 🆔, or ⛔ — follow up with assign_block_id on next_occurrence.line, except on an indented line of a Kanban board. Completing by line is NOT idempotent for recurring tasks (the spawn occupies the old line); prefer block_id. - Delete (🏁 delete / [onCompletion:: delete]), on "done": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. Result carries on_completion_applied: "delete". + Delete (🏁 delete / [onCompletion:: delete]), on "done": removes the task line and its children instead of moving to done. With 🔁 + 🏁, the next occurrence is still created, and the completed task is removed. - recurrence: a rule ending "when done" bases the next occurrence on the completion day. When set together with status "done", the new rule governs the next occurrence. Setting recurrence to null while completing removes the rule and completes without spawning. - on_completion: passed together with status, the submitted value governs the delete decision — "keep" while completing a "delete" task prevents the deletion. - position: applies to a heading move or an auto-done-lane move; otherwise it reorders the task within its current lane. Omitted, an unmoved task stays put. Ignored when the task is deleted on completion. Errors: (messages use camelCase names, e.g. assignBlockId for assign_block_id) -- "note not found" — path does not exist; check the path${whenToolEnabledText("vault_list_notes", " with vault_list_notes")} -- "path must end in …" — add the .md extension -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "exactly one of blockId or line is required" / "blockId and line are mutually exclusive" — pass exactly one of block_id or line +${noteNotFoundErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +- "exactly one of blockId or line is required" / "blockId and line are mutually exclusive" — the message states the rule - "blockId ... not found" / "blockId ... is inside a fenced code block or comment" — no task line outside a fenced code block or %% %% comment ends with that ^id; check the id${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")}, or target by line - "blockId ... matches N task lines" — more than one task line ends in the id (the message lists their line numbers); target the intended task by line. To repair on a regular note, give all but one line a new id with assign_block_id. On a Kanban board, where the plugin copied a sub-task's id onto its card, remove the id from the sub-task's line (assign_block_id: null, by line), then assign the card the id its existing links use, or a new one - "no task at line N" / "line N is inside a fenced code block or comment" — the line holds no task checkbox, or sits inside a fenced code block or %% %% comment; re-read line numbers${whenToolEnabledText("vault_list_tasks", " with vault_list_tasks")}, or target by block_id - "checkbox "[c]" is a NON_TASK status" / "no checkbox symbol for status ..." — in the Tasks plugin's status settings, the targeted task's checkbox character (c) is typed NON_TASK so it is not a task, or the status the message names has no symbol and its default character has another type; the user must change that character's type or add a symbol for that status there, then restart the server -- "at least one mutation" — no change params provided; pass at least one field to change +- "at least one mutation" — the message lists the fields that count - "cannot move a sub-task to a heading" / "cannot reposition a sub-task" — a heading or position on a task nested under another task (depth > 0${whenToolEnabledText("vault_list_tasks", " in vault_list_tasks")}); sub-tasks move with their parent, so move or reposition the parent instead -- "cannot reorder a task that sits above the first heading" — position without a heading on a task before the first section heading; pass heading to move it into a section +- "cannot reorder a task that sits above the first heading" — position without a heading on a task before the first section heading; the message names the fix - "cannot reorder within "X" — the heading appears N times" / "cannot place at position P under "X" — the heading appears N times" — the heading name is duplicated in the note: the task's own heading for a reorder in place, the one in heading for a move; for a cross-lane move use "top" or "bottom" (they place under the first match), or rename one section to make it unique -- "heading "X" not found; available: ..." — target heading doesn't exist; the error lists the note's headings -- "multiple done lanes detected" / "no done lane detected" — status "done" on a Kanban board with more than one lane holding a **Complete** line, or with none and no "Done" heading; pass heading to pick the lane +${TASK_HEADING_NOT_FOUND_ERROR_ENTRY} +- "multiple done lanes detected" / "no done lane detected" — status "done" on a Kanban board with more than one lane whose first line is **Complete**, or with none and no "Done" heading; pass heading to pick the lane - "blockId ... already exists" / "blockId ... contains invalid characters" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+ - "assignBlockId is not allowed" / "description ends in a block ID" / "subtask ... ends in a block ID" — a new ^id on an indented line of a Kanban board; leave the line without an id and target it by line, and drop any trailing ^id from the text -- "invalid date: …" — a date param isn't in YYYY-MM-DD form or names a day that doesn't exist; pass a real YYYY-MM-DD date +${TASK_DATE_ERROR_ENTRY} - "description cannot be empty" / "addSubtasks cannot contain an empty item" / "description must be a single line" / "addSubtasks items must be a single line" — pass visible text on a single line -- "taskId ... contains invalid characters" / "dependsOn entry ... contains invalid characters" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ -- "unrecognized recurrence rule ..." — the rule text is not Tasks-plugin natural language; use a rule such as "every week" or "every 2 weeks when done" -- "concurrent write in progress" — another write to this note is in flight; retry +${TASK_ID_CHARACTERS_ERROR_ENTRY} +${RECURRENCE_RULE_ERROR_ENTRY} +${CONCURRENT_WRITE_RETRY_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${describePropertiesBlockErrorEntry()} ${OPENING_BLOCK_ERROR_ENTRY} -Obsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like "🔁 every week", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. Whether it is captured depends on the field's value grammar: 🔁 reads any trailing words as its recurrence rule, while 📅 followed by non-date words stays description text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. A description signifier that changes how a stamped date parses back is still reported. +Obsidian syntax: When description or add_subtasks text ends in a signifier the plugin's parser recognizes as a field (an emoji field like "🔁 every week", or a Dataview [key:: value] field), with only other recognized fields after it, that text is read back as metadata, not text. The write still succeeds either way; reword the text to clear any advisories the result carries. The ✅/❌ dates a status change adds or removes produce no advisories by themselves. Returns: JSON { path, line, description, block_id, heading, subtasks, next_occurrence, changes, advisories, on_completion_applied }. - line is the task's final 1-based line number in the file; after an on_completion delete, the line the task occupied before removal. - description is the current text, and block_id and heading reflect the task after the update; block_id is omitted when the task has none, heading when the task sits above the first heading. - subtasks lists each checklist item add_subtasks wrote as { line, description } (omitted when none were added); checklist items are written without a block_id, so line is the handle for a follow-up update. - next_occurrence is present only when a completion spawned a recurring task's next occurrence: { line, description, due?, scheduled?, start? } with only the dates the occurrence has. -- changes lists every field applied as "field: before → after", with "(none)" for an absent value. An add_subtasks change appears as "subtasks", its two sides checklist-item counts, and a spawn adds "next_occurrence: (none) → line N". +- changes lists every field applied as "field: before → after", with "(none)" for an absent value; an add_subtasks change is listed under "subtasks", a spawn under "next_occurrence", and a 🏁 delete under "on_completion". - advisories (omitted when there are none) lists one sentence for each: a stored line that parses back differently than submitted (see Obsidian syntax above), a duplicate tag removed from the line, or a completed recurring task whose rule yields no next occurrence (unreadable rule text, or a finite rule with no occurrences left). - on_completion_applied is "delete", present only when the effective on_completion was delete (already on the task or set in the same call) and the task was transitioned to done.`, inputSchema: { @@ -536,14 +562,7 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, next_occur .describe( "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", ), - line: z - .number() - .int() - .min(1) - .optional() - .describe( - "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", - ), + line: z.number().int().min(1).optional().describe(updateTaskLineDescription), status: z .enum(["todo", "in_progress", "done", "cancelled"]) .optional() @@ -574,9 +593,7 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, next_occur .string() .min(1) .optional() - .describe( - "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", - ), + .describe("New task description text; metadata fields and block_id are preserved."), due: z .string() .min(1) @@ -622,7 +639,7 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, next_occur .min(1) .optional() .describe( - `Checklist items to append, one indented todo line each, under the task's existing items — never replaces them. Can be combined with any other change; not appended, and not reported, when the same call removes the task (on_completion delete).${whenToolEnabledText("vault_create_task", " For full sub-tasks with metadata, use vault_create_task with parent_block_id.")}`, + `Checklist items to append, one indented todo line each, under the task's existing items. Not appended, and not reported, when the same call removes the task (on_completion delete).${whenToolEnabledText("vault_create_task", " For full sub-tasks with metadata, use vault_create_task with parent_block_id.")}`, ), assign_block_id: z .string() @@ -643,7 +660,7 @@ Returns: JSON { path, line, description, block_id, heading, subtasks, next_occur .union([z.enum(["top", "bottom"]), z.number().int().min(1)]) .optional() .describe( - 'Where within the target heading the task lands. "top" or "bottom" for the extremes; an integer (1-based) for an exact position among the lane\'s top-level cards (sub-tasks move with their parent and are not counted). Position 1 is the first card. A position past the card count lands directly below the last card (unlike "bottom", which appends after any non-task text at the end of the section). Defaults to "top" on heading and done-lane moves.', + 'Where within the target heading the task lands. "top" or "bottom" for the extremes; an integer (1-based) for an exact position among the lane\'s top-level cards (sub-tasks move with their parent and are not counted). A position past the card count lands directly below the last card (unlike "bottom", which appends after any non-task text at the end of the section). Defaults to "top" on heading and done-lane moves.', ), format: z .enum(["emoji", "dataview"]) diff --git a/src/vault-mcp/mcp-core/tools/tool-helpers.ts b/src/vault-mcp/mcp-core/tools/tool-helpers.ts index 2010f8615..47e14dbe3 100644 --- a/src/vault-mcp/mcp-core/tools/tool-helpers.ts +++ b/src/vault-mcp/mcp-core/tools/tool-helpers.ts @@ -57,6 +57,15 @@ export const createStrictInputObject = ( }) } +/** Sentences, any of them "" when gated off, as one line that starts with a + * line break. Empty sentences drop out before the join, so none leaves a + * leading space or an empty line. */ +export const formatServedSentencesLine = (gatedSentences: readonly string[]): string => { + const servedSentences = gatedSentences.filter(Boolean).join(" ") + + return servedSentences ? `\n${servedSentences}` : "" +} + export type ToolRegistrationContext = ToolAvailability & SafeHandlers & { registerTool: RegisterGatedTool @@ -242,6 +251,19 @@ export const OPENING_BLOCK_ERROR_ENTRY = export const FILESYSTEM_ERROR_ENTRY = '- "ENAMETOOLONG: …", "EACCES: …" or another filesystem error code — the message says what to do' +/** The Errors entry of a tool that resolves a single note or file path. */ +export const PATH_SAFETY_ERROR_ENTRY = + '- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it' + +/** The Errors entry of a tool whose path must name a markdown note. */ +export const NOTE_EXTENSION_ERROR_ENTRY = '- "path must end in …" — add the .md extension' + +/** The Errors entry of a tool whose write lock refused the call, when a plain + * retry is the remedy. The lock refuses a second write to a note instead of + * queueing it. */ +export const CONCURRENT_WRITE_RETRY_ERROR_ENTRY = + '- "concurrent write in progress" — another write to this note is in flight; retry' + /** The listing tools' form of the entry: one unreadable folder fails the whole * listing, and the caller can list around it. */ export const LISTING_FILESYSTEM_ERROR_ENTRY = diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 0a35696bb..f0b92648b 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -14,15 +14,33 @@ import { pageTextByLines } from "../../obsidian-markdown/lines.js" import { TOOL_NAMES } from "../tool-registry.js" import type { ToolRegistrationContext } from "./tool-helpers.js" import { + CONCURRENT_WRITE_RETRY_ERROR_ENTRY, FILESYSTEM_ERROR_ENTRY, - INDEX_ENGINE_ERROR_ENTRY, LISTING_FILESYSTEM_ERROR_ENTRY, describePropertiesBlockErrorEntry, describeTextWindow, + formatServedSentencesLine, + NOTE_EXTENSION_ERROR_ENTRY, OPENING_BLOCK_ERROR_ENTRY, + PATH_SAFETY_ERROR_ENTRY, toolInputErrorResult, } from "./tool-helpers.js" +// Errors entries that two or more of this module's tools share word for word. +const HEADING_NOT_FOUND_ERROR_ENTRY = + '- "heading not found" — the message lists the available headings' +const EMPTY_HEADING_ERROR_ENTRY = + '- "heading cannot be empty" — heading is whitespace only; pass the heading\'s text' +const CONTENT_CONTROL_CHARACTER_ERROR_ENTRY = + '- "content contains a control character" — remove the character the message names' +const AMBIGUOUS_SPAN_ANCHOR_ERROR_ENTRY = + '- "ambiguous start anchor …" / "ambiguous end anchor …" — the anchor matches multiple lines; use a longer fragment or set first_match: true' + +/** The Errors entry of a tool whose write lock refused the call, when the caller + * re-reads the note before retrying because the other write may have changed it. */ +const CONCURRENT_WRITE_REREAD_ERROR_ENTRY = + '- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry' + /** Advisory sentence for a no-heading prepend that nested pre-existing content * inside the heading it inserted. Names the remedy as a vault_patch_note * operation — routing guidance is the tool layer's job, so the data layer @@ -56,15 +74,6 @@ const describeProtectedPaths = (config: VaultConfig): string => { return `${config.memoryDir}/ and the daily notes folder (${DEFAULT_DAILY_NOTES_FOLDER}/ by default; not protected when it is the vault root or blank)` } -/** Sentences that point to other tools, each already "" when its tool is - * disabled, as one line that starts with a line break. Empty sentences drop out - * before the join, so none leaves a leading space or an empty line. */ -const formatServedSentencesLine = (gatedSentences: readonly string[]): string => { - const servedSentences = gatedSentences.filter(Boolean).join(" ") - - return servedSentences ? `\n${servedSentences}` : "" -} - export const registerVaultCrudTools = ({ registerTool, isToolEnabled, @@ -77,41 +86,98 @@ export const registerVaultCrudTools = ({ logger: sessionLogger, config, }: ToolRegistrationContext): void => { + // The read, edit, and delete tools' not-found remedies name vault_list_notes or + // vault_read_note only while that tool is served; otherwise they fall back to a + // check that needs no tool. Each remedy keeps its own tools' wording, so the + // three note-not-found remedies differ by a word or two. + const listNotesEnabled = isToolEnabled("vault_list_notes") + const noteNotFoundCheckRemedy = listNotesEnabled + ? "check vault_list_notes for valid paths" + : "check its spelling and letter case" + const noteNotFoundVerifyRemedy = listNotesEnabled + ? "verify path with vault_list_notes" + : "check the path's spelling and letter case" + const readNoteNotFoundRemedy = listNotesEnabled + ? "verify it with vault_list_notes" + : "check its spelling and letter case" + const readNoteEnabled = isToolEnabled("vault_read_note") + const textNotFoundRemedy = readNoteEnabled + ? "verify exact text with vault_read_note" + : "check old_text's letter case, spacing, and line breaks" + const anchorNotFoundRemedy = readNoteEnabled + ? "verify with vault_read_note" + : "check the fragment's letter case and spacing" + + // Errors entries built from those remedies that two or more tools share word for word. + const noteNotFoundCheckErrorEntry = `- "note not found" — path does not exist; ${noteNotFoundCheckRemedy}` + const noteNotFoundVerifyErrorEntry = `- "note not found" — ${noteNotFoundVerifyRemedy}` + const spanAnchorNotFoundErrorEntry = `- "start anchor not found" / "end anchor not found" — no body line holds the fragment where it is searched; ${anchorNotFoundRemedy}` + + // Incoming and outgoing links are separate tools, so the sentence names + // whichever of the pair is served. + const describeReadNoteLinkTools = (): string => { + const backlinksEnabled = isToolEnabled("vault_get_backlinks") + const outgoingLinksEnabled = isToolEnabled("vault_get_outgoing_links") + + if (backlinksEnabled && outgoingLinksEnabled) { + return "To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links." + } + if (backlinksEnabled) { + return "To explore what links to this note, use vault_get_backlinks." + } + if (outgoingLinksEnabled) { + return "To explore what this note links to, use vault_get_outgoing_links." + } + return "" + } + + const readNoteAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText("vault_search", "Prefer vault_search when you don't know the path."), + whenToolEnabledText( + "vault_list_tasks", + "For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown.", + ), + whenToolEnabledText( + "vault_get_memory", + `Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties).`, + ), + whenToolEnabledText("vault_patch_note", "To edit a section you've read, use vault_patch_note."), + describeReadNoteLinkTools(), + ]) + registerTool( TOOL_NAMES.VAULT_READ_NOTE, { title: "Read Note", - description: `Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading. + description: `Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section. Example: vault_read_note({ path: "Projects/vault-cortex.md" }) -Example: vault_read_note({ path: "Projects/vault-cortex.md", properties_only: true }) Example: vault_read_note({ path: "TASKS.md", outline: true }) Example: vault_read_note({ path: "TASKS.md", heading: "Active" }) -Example: vault_read_note({ path: "TASKS.md", heading: "Done", heading_level: 2 }) // disambiguate when several "Done" headings exist -Example: vault_read_note({ path: "TASKS.md", heading: "Done", start_line: 1, limit: 20 }) // first 20 lines of an oversized section +Example: vault_read_note({ path: "TASKS.md", heading: "Done", heading_level: 2 }) +Example: vault_read_note({ path: "TASKS.md", heading: "Done", start_line: 1, limit: 20 }) -When to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: "..." to read just the one section you need. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total. -Prefer vault_search when you don't know the path.${whenToolEnabledText("vault_list_tasks", " For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown.")}${whenToolEnabledText("vault_get_memory", ` Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties).`)}${whenToolEnabledText("vault_patch_note", " To edit a section you've read, use vault_patch_note.")} To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links. +When to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: "..." to read just the one section you need. For a note or section too long for one response (this tool sets no cap), page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.${readNoteAlternativesLine} Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Errors: -- "note not found" — no note exists at this path; verify it with vault_list_notes -- "heading not found" — no heading matches the text; error lists available headings +- "note not found" — no note exists at this path; ${readNoteNotFoundRemedy} +${HEADING_NOT_FOUND_ERROR_ENTRY} - "ambiguous heading" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level -- "outline, heading, and properties_only are mutually exclusive" — only one mode per call -- "heading_level requires a heading" — heading_level only disambiguates a heading; pass heading with it -- "heading cannot be empty" — heading is whitespace only; pass the heading's text -- "line paging is not available in outline mode" / "... properties_only mode" — start_line/limit only work on text renditions (full read or heading section) -- "start line past the end" — start_line exceeds the rendition's line count; error states the total +- "outline, heading, and properties_only are mutually exclusive — set at most one" +- "heading_level requires a heading" — pass heading with it +${EMPTY_HEADING_ERROR_ENTRY} +- "line paging is not available in outline mode" / "… properties_only mode" — start_line and limit page only a full read or a heading section +- "start line past the end" — the message states the line count; resend a start_line within it - 'path must end in ".md"' — add the extension to a note's path${whenToolEnabledText("vault_read_file", "; read other files (images, .canvas, data files) with vault_read_file")} -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it +${PATH_SAFETY_ERROR_ENTRY} - "properties block is not valid YAML …" — a full read, outline and heading still work; the error says how to repair it ${FILESYSTEM_ERROR_ENTRY} Returns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block ("path — lines 1–20 of 250 (continue with start_line: 21)"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings. -Outline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings ("##" with no text) appear with text: "" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note${whenToolEnabledText("vault_replace_in_note", ", and edit via vault_replace_in_note")}.`, +Outline: bytes at the root is the whole file's size and modified is its modification time (ISO 8601); each heading's bytes is the size of the text that heading mode returns for that section. Empty headings ("##" with no text) appear with text: "" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section or the full note${whenToolEnabledText("vault_replace_in_note", ", and edit via vault_replace_in_note")}.`, inputSchema: { path: z .string() @@ -127,7 +193,7 @@ Outline: bytes at the root is the whole file's on-disk size and modified is its .boolean() .optional() .describe( - "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content — a cheap structure fetch for large notes. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", + "If true, returns { bytes, modified, leading_callout?, leading_content?, headings } as JSON instead of body content. headings: [{ level, text, bytes }]; leading_callout: { type, title, body } when the note has a top-of-file callout; leading_content: the rest of the body text above the first heading (callout lines excluded) when the note has any.", ), heading: z .string() @@ -141,16 +207,14 @@ Outline: bytes at the root is the whole file's on-disk size and modified is its .max(6) .optional() .describe( - "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "Heading level (1-6) for disambiguation when multiple headings share the same text", ), start_line: z .number() .int() .min(1) .optional() - .describe( - "First line to return, 1-based (default 1). Pages the text output (full body or a heading section). Not valid for outline or properties_only (JSON modes).", - ), + .describe("First line to return, 1-based (default 1)."), limit: z .number() .int() @@ -297,6 +361,15 @@ Outline: bytes at the root is the whole file's on-disk size and modified is its }, ) + const listNotesAlternativesLine = formatServedSentencesLine([ + whenToolEnabledText( + "vault_search_by_folder", + "Prefer vault_search_by_folder when you need metadata (tags, type, related) along with paths.", + ), + whenToolEnabledText("vault_search", "Prefer vault_search for content-based discovery."), + whenToolEnabledText("vault_read_note", "Use vault_read_note to read a note from the results."), + ]) + registerTool( TOOL_NAMES.VAULT_LIST_NOTES, { @@ -307,11 +380,10 @@ Example: vault_list_notes({ folder: "Projects" }) Example: vault_list_notes({ glob: "**/*session-log*.md" }) Example: vault_list_notes({ folder: "Projects", glob: "*.md" }) — the folder's top-level notes only -When to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern. -Prefer vault_search_by_folder when you need metadata (tags, type, related) along with paths. Prefer vault_search for content-based discovery. Use vault_read_note to read a note from the results. +When to use: Browsing what exists in a folder by filename, or finding notes matching a path pattern.${listNotesAlternativesLine} Parameters: -- folder names a whole folder; without a glob, notes in its subfolders are listed too: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Use the folder's exact letter case, as other results show it; on a case-sensitive filesystem a different case finds nothing. +- folder names a whole folder; without a glob, notes in its subfolders are listed too: "Projects" covers "Projects/Archive" but not "ProjectsOld/". Use the folder's exact letter case; on a case-sensitive filesystem a different case finds nothing. - glob matches each note's path inside folder (its vault-relative path when folder is omitted), case-sensitively. * stays within one folder level and ** spans any depth: with folder "Projects", "**/*.md" lists every note under it. Behavior: Paths come back sorted by vault-relative path in code-unit order (uppercase before lowercase). Hidden (dot-prefixed) notes and folders are never listed; symlinked notes are included. @@ -321,7 +393,7 @@ Errors: - "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative folder outside hidden folders, or omit folder to list the whole vault. ${LISTING_FILESYSTEM_ERROR_ENTRY}. -Returns: JSON array of vault-relative path strings (e.g. ["Notes/idea.md", "Projects/plan.md"]).`, +Returns: JSON array of every matching vault-relative path, with no cap (e.g. ["Notes/idea.md", "Projects/plan.md"]).`, inputSchema: { folder: z .string() @@ -354,14 +426,12 @@ Returns: JSON array of vault-relative path strings (e.g. ["Notes/idea.md", "Proj }, ) - // The "note already exists" remedy names only the partial-edit tools the - // server serves, and drops the clause when it serves neither. + // The routing line names only the partial-edit tools the server serves, and + // drops out when it serves neither. const partialEditToolNames = formatEnabledToolList(["vault_patch_note", "vault_replace_in_note"]) - const partialEditAdvice = partialEditToolNames - ? `, or use ${partialEditToolNames} for partial edits` - : "" const writeNoteAlternativeLines = [ + partialEditToolNames ? `Prefer ${partialEditToolNames} to change part of a note.` : "", whenToolEnabledText( "vault_update_properties", "Prefer vault_update_properties for property-only edits (no body round-trip).", @@ -379,23 +449,23 @@ Returns: JSON array of vault-relative path strings (e.g. ["Notes/idea.md", "Proj TOOL_NAMES.VAULT_WRITE_NOTE, { title: "Write Note", - description: `Create a markdown note. Errors if a note already exists at the path unless overwrite is set. Body replaces the entire note content: existing content will be lost unless you include it in body, so do not use this tool for surgical edits to large files. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null. + description: `Create a markdown note, or with overwrite: true replace an existing note's body; the old body's text is lost unless the new body includes it. Properties merge with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved); overwriting without properties keeps the existing property values. A new note leaves out keys set to null. Example: vault_write_note({ path: "Projects/notes.md", body: "# Notes\\n\\nProject notes here.", properties: { tags: ["project"], type: "project" } }) Example: vault_write_note({ path: "Projects/notes.md", body: "Updated content.", overwrite: true }) -When to use: Creating a new note. Set overwrite: true only when you intend to replace an existing note's body.${writeNoteAlternativeLines} +When to use: Creating a new note, or replacing a whole note's body.${writeNoteAlternativeLines} Errors: -- "note already exists" — set overwrite: true to replace it${partialEditAdvice} -- "path must end in …" — add the .md extension +- "note already exists" — set overwrite: true to replace it +${NOTE_EXTENSION_ERROR_ENTRY} - "cannot write note …: that path is not a file" — a folder already has this name; choose another path -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry -- "body contains a control character" — body includes a non-printable control byte; remove it before writing +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} +- "body contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${describePropertiesBlockErrorEntry("with overwrite: true")} -- "the note would open with a properties block the server cannot keep …" — the note would get no properties (none passed or kept from an overwritten note, or every key set to null), and the body opens with --- lines around text that is invalid YAML, a list, a single value, or a YAML tag, which would become its properties block; pass at least one property, or start the body without --- lines +${OPENING_BLOCK_ERROR_ENTRY} Obsidian syntax: Body is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag (escape with \\#), [[ = wikilink, %% = comment block. In properties: quote wikilink values ("[[Note]]"), use YAML lists for tags, keep property types consistent (string/number/list mismatches cause silent query failures). @@ -453,6 +523,7 @@ Returns: "Wrote ".`, Editing a leading callout: read it via vault_read_note(outline: true), then vault_replace_in_note the old block for the new one (a no-heading prepend would stack a second callout above it).` : "" + const patchNoteAlternativesLine = formatServedSentencesLine([ whenToolEnabledText( "vault_write_note", @@ -482,23 +553,6 @@ Editing a leading callout: read it via vault_read_note(outline: true), then vaul ), ]) - // The edit and delete tools' not-found remedies name vault_list_notes or vault_read_note - // only while that tool is served; otherwise they fall back to a check that needs no tool. - const listNotesEnabled = isToolEnabled("vault_list_notes") - const noteNotFoundCheckRemedy = listNotesEnabled - ? "check vault_list_notes for valid paths" - : "check its spelling and letter case" - const noteNotFoundVerifyRemedy = listNotesEnabled - ? "verify path with vault_list_notes" - : "check the path's spelling and letter case" - const readNoteEnabled = isToolEnabled("vault_read_note") - const textNotFoundRemedy = readNoteEnabled - ? "verify exact text with vault_read_note" - : "check old_text's letter case, spacing, and line breaks" - const anchorNotFoundRemedy = readNoteEnabled - ? "verify with vault_read_note" - : "check the fragment's letter case and spacing" - registerTool( TOOL_NAMES.VAULT_PATCH_NOTE, { @@ -514,32 +568,30 @@ When to use: Modifying part of an existing note without overwriting the entire b Operations: - append: add content at end of section (or end of file if no heading), below any empty lines that end it, so appending a row to a table that a blank line follows starts a new table${whenToolEnabledText("vault_insert_at_anchor", "; to extend a table, use vault_insert_at_anchor after its last row")} -- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading, not a no-heading prepend. +- prepend: add content after heading line (or at the top of the body, below frontmatter, if no heading — how you add a leading callout). To start a new section above the note's current first heading, use insert_before on that heading: a no-heading prepend whose content starts with a heading nests any content above the first heading inside the new section. - replace: replace section body (requires heading)${whenToolEnabledText("vault_delete_span", "; to empty it, use vault_delete_span on its lines")} - insert_before: insert content above the heading line (requires heading) Heading-targeted ops keep the matched heading. Every operation writes content verbatim as whole lines with no separator: end content with a newline to leave a blank line after it; without one, the next line follows directly. -Limitation: If a no-heading prepend's content starts with a heading and the note has content above its first heading, that content becomes the new section's body. The write still succeeds. - Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF), so it includes its child headings. Empty headings ("##" with no text) act as boundaries but cannot be targeted${whenToolEnabledText("vault_replace_in_note", " — edit their content via vault_replace_in_note instead")}.${leadingCalloutEditText} Errors: -- "note not found" — path does not exist; ${noteNotFoundCheckRemedy} -- "path must end in …" — add the .md extension -- "heading not found" — no heading matches the text; error lists available headings +${noteNotFoundCheckErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +${HEADING_NOT_FOUND_ERROR_ENTRY} - "ambiguous heading" — multiple headings match; use heading_level to disambiguate; if they share a level, rename one${whenToolEnabledText("vault_replace_in_note", " or use vault_replace_in_note to target by text content")} - "operation … requires a heading target" — pass heading, or use append or prepend to edit the file body -- "heading cannot be empty" — heading is whitespace only; pass the heading's text +${EMPTY_HEADING_ERROR_ENTRY} - "content begins with the heading … which would duplicate it" — content's first line repeats the target heading; omit it - "section … has N child headings …" — the target section contains child headings that replace would destroy; pass include_children: true to confirm, or target the child heading directly -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry -- "content contains a control character" — content includes a non-printable control byte; remove it before writing +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} +${CONTENT_CONTROL_CHARACTER_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} -Obsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Inserting heading-level content (## New Section) changes the note's structure — future heading-targeted ops may resolve differently. +Obsidian syntax: Content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block. Table rows: send only the data row ("| cell1 | cell2 |"), not the header or separator — duplicating them splits the table. Returns: Confirmation message — "Applied to → ", where target is the matched heading (e.g. "## Active") or "file body" for a no-heading append/prepend. A no-heading prepend that nested existing content under an inserted heading adds a sentence naming the content's size and any call that would have avoided it.`, @@ -652,26 +704,30 @@ Example: vault_replace_in_note({ path: "Projects/plan.md", old_text: "TODO: writ Example: vault_replace_in_note({ path: "Projects/plan.md", old_text: "- [ ] draft outline\\n", new_text: "" }) Example: vault_replace_in_note({ path: "Projects/plan.md", old_text: "Q3", new_text: "Q4", replace_all_occurrences: true }) -When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text=""). Replaces text in place; does not move content across sections.${replaceInNoteAlternativesLine} +When to use: Targeted text changes within a single location — fixing typos, updating values, renaming terms, or removing a short line (new_text="").${replaceInNoteAlternativesLine} Parameters: - old_text: include enough surrounding context to ensure uniqueness when the target text appears in multiple places. -- new_text: non-empty new_text replaces the match exactly. A deletion (new_text="") that leaves text on its line is written exactly as asked. One that empties its line never doubles blank lines: the text above and below ends up separated by the wider of the two runs of empty lines that stood there, and by at least one; at the note's end the run above counts one fewer ("A\\n\\nB\\n" without "B" → "A\\n"). So include the line break in old_text to remove a table row or list item, or the empty line left behind splits the table or list. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line. +- new_text: a non-empty value replaces the match exactly. For a deletion (new_text=""): + - When text remains on the line, it is written exactly as asked. + - When the line empties, blank lines never double: the text above and below is separated by the wider of the two runs of empty lines that stood there, and by at least one. At the note's end the run above counts one fewer ("A\\n\\nB\\n" without "B" → "A\\n"). + - To remove a table row or list item, include its line break in old_text, or the empty line left behind splits the table or list. + - Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line. - replace_all_occurrences: set true for deliberate bulk renames or term replacements. Errors: -- "note not found" — path does not exist; ${noteNotFoundCheckRemedy} -- "path must end in …" — add the .md extension +${noteNotFoundCheckErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} - "text not found" — old_text does not appear in the note body; ${textNotFoundRemedy} -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry -- "new_text contains a control character" — new_text includes a non-printable control byte; remove it before writing +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} +- "new_text contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} Obsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text. -Returns: "Replaced occurrence(s) in " — N is the number of matches replaced.`, +Returns: "Replaced occurrence(s) in ".`, inputSchema: { path: z .string() @@ -762,17 +818,17 @@ Example: vault_delete_span({ path: "Notes/Plan.md", start_anchor: "> [!warning] When to use: Removing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.${deleteSpanAlternativesLine} Parameters: -- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is deleted. ${fencedBlockEndAnchorText("remove")} +- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is deleted. ${fencedBlockEndAnchorText("remove")} Behavior: Blank lines never double: the text above and below the removed lines ends up separated by the wider of the two runs of empty lines that stood there; at the note's end the run above counts one fewer ("A\\n\\nB\\n" without "B" → "A\\n"). So removing a table row leaves the table whole. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line. Errors: -- "note not found" — ${noteNotFoundVerifyRemedy} -- "path must end in …" — add the .md extension -- "start anchor not found" / "end anchor not found" — no line contains the fragment (for end_anchor, none at or after the start line); ${anchorNotFoundRemedy} -- "ambiguous start anchor …" / "ambiguous end anchor …" — the anchor matches multiple lines; use a longer fragment or set first_match: true -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry +${noteNotFoundVerifyErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +${spanAnchorNotFoundErrorEntry} +${AMBIGUOUS_SPAN_ANCHOR_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -788,14 +844,14 @@ Returns: 'Deleted line(s) from : ""' — the removed li .string() .min(1) .describe( - "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block.", + "Short, unique substring of the block's first line, a body line (case-sensitive).", ), end_anchor: z .string() .min(1) .optional() .describe( - "Short substring that identifies the LAST line of the block (case-sensitive). Omit to delete just the single line containing start_anchor.", + "Short substring of the block's LAST line, a body line (case-sensitive). Omit to delete just the single line containing start_anchor.", ), first_match: z .boolean() @@ -861,17 +917,20 @@ Example: vault_replace_span({ path: "Notes/Plan.md", start_anchor: "> [!warning] When to use: Replacing a block you have already read — a table row, callout, or run of list items — where reproducing it exactly as old_text would be error-prone.${replaceSpanAlternativesLine} Parameters: -- end_anchor is searched at or after the start line and must be unique among those lines. If both match the same line, only that one line is replaced. ${fencedBlockEndAnchorText("replace")} -- content: line breaks at its start or end merge with the note's empty lines on that side, keeping the wider run (at the note's end, content's trailing line breaks count one fewer). So content can widen a gap but never narrow it: content with no line break at either end keeps the gaps as they were, and content ending in one ("| X |\\n") leaves an empty line after it, which splits a table. Content made only of line breaks joins both sides into one gap. Empty lines inside content are written as given. Empty lines elsewhere in the note stay as they are, and a line holding only spaces or tabs counts as text, not as an empty line. +- end_anchor is searched at or after the start line and must be unique among those lines. If start_anchor and end_anchor match the same line, only that one line is replaced. ${fencedBlockEndAnchorText("replace")} +- content's line breaks: + - A line break at its start or end merges with the note's empty lines on that side, keeping the wider run. At the note's end, its trailing line breaks count one fewer. + - Content ending in a line break ("| X |\\n") leaves an empty line after it, which splits a table. + - Empty lines inside content and elsewhere in the note are kept as written, and a line holding only spaces or tabs counts as text, not as an empty line. Errors: -- "note not found" — ${noteNotFoundVerifyRemedy} -- "path must end in …" — add the .md extension -- "start anchor not found" / "end anchor not found" — no line contains the fragment (for end_anchor, none at or after the start line); ${anchorNotFoundRemedy} -- "ambiguous start anchor …" / "ambiguous end anchor …" — the anchor matches multiple lines; use a longer fragment or set first_match: true -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry -- "content contains a control character" — content includes a non-printable control byte; remove it before writing +${noteNotFoundVerifyErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +${spanAnchorNotFoundErrorEntry} +${AMBIGUOUS_SPAN_ANCHOR_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} +${CONTENT_CONTROL_CHARACTER_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -889,21 +948,16 @@ Returns: Confirmation message "Replaced line(s) with line(s) in " .string() .min(1) .describe( - "Short, unique substring that identifies the first line of the block (case-sensitive). Pick a brief fragment — do not paste the whole block.", + "Short, unique substring of the block's first line, a body line (case-sensitive).", ), end_anchor: z .string() .min(1) .optional() .describe( - "Short substring that identifies the LAST line of the block (case-sensitive). Omit to replace just the single line containing start_anchor.", - ), - content: z - .string() - .min(1) - .describe( - "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty.", + "Short substring of the block's LAST line, a body line (case-sensitive). Omit to replace just the single line containing start_anchor.", ), + content: z.string().min(1).describe("Replacement content (one or more lines)."), first_match: z .boolean() .optional() @@ -961,7 +1015,7 @@ Returns: Confirmation message "Replaced line(s) with line(s) in " TOOL_NAMES.VAULT_INSERT_AT_ANCHOR, { title: "Insert at Anchor", - description: `Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. + description: `Insert content as whole lines into a note's body, before or after a specific line identified by a short anchor substring. Properties are kept as written, and CRLF line breaks are saved as LF. A failed save leaves the note unchanged. Example: vault_insert_at_anchor({ path: "Tracker.md", anchor: "| 2024-03-02 | Acme", position: "after", content: "| 2024-03-03 | Beta Corp | New entry |" }) — inserts a new table row after the matched row. Example: vault_insert_at_anchor({ path: "Notes/Plan.md", anchor: "## Phase 2", position: "before", content: "> [!note] Phase 1 must close before this starts.\\n" }) — inserts a callout and a blank line above the Phase 2 heading. @@ -972,13 +1026,13 @@ Parameters: - content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block. Errors: -- "note not found" — ${noteNotFoundVerifyRemedy} -- "path must end in …" — add the .md extension -- "anchor not found" — fragment not on any line; ${anchorNotFoundRemedy} +${noteNotFoundVerifyErrorEntry} +${NOTE_EXTENSION_ERROR_ENTRY} +- "anchor not found" — fragment not on any body line; ${anchorNotFoundRemedy} - "ambiguous anchor …" — the anchor matches multiple lines; use a longer fragment or set first_match: true -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry -- "content contains a control character" — content includes a non-printable control byte; remove it before writing +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} +${CONTENT_CONTROL_CHARACTER_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -995,13 +1049,11 @@ Returns: Confirmation message "Inserted line(s) anchor in line(s) anchor in " for permanent removal, "Moved to trash (<.trash/ path>)" when the note landed in .trash/.`, } + const protectedPathRemedy = isToolEnabled("vault_delete_memory") + ? "use vault_delete_memory for memory entries" + : "ask the vault owner to delete it in Obsidian" + registerTool( TOOL_NAMES.VAULT_DELETE_NOTE, { @@ -1091,14 +1147,14 @@ Parameters: - prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. Pruning is best-effort: a folder that can't be removed never fails the call. Errors: -- "cannot delete protected path" — the path sits under a protected folder${whenToolEnabledText("vault_delete_memory", "; use vault_delete_memory for memory entries")} -- "path must end in …" — add the .md extension -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; retry +- "cannot delete protected path" — the path sits under a protected folder; ${protectedPathRemedy} +${NOTE_EXTENSION_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_RETRY_ERROR_ENTRY} - "note not found: …" — ${noteNotFoundVerifyRemedy}${deleteNoteText.trashMoveErrorEntries} - "cannot delete …" (other than a protected path) — the note stays put; ask the vault owner to fix the cause (e.g. permissions), then retry ${FILESYSTEM_ERROR_ENTRY}; the note stays put${deleteNoteText.trashConfigErrorEntry} -- "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable; ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry +- "cannot read daily notes config from .obsidian/daily-notes.json" — ask the vault owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry Returns: ${deleteNoteText.returns} Notes how many empty folders were pruned when any were.`, inputSchema: { @@ -1210,7 +1266,7 @@ Returns: ${deleteNoteText.returns} Notes how many empty folders were pruned when TOOL_NAMES.VAULT_MOVE_NOTE, { title: "Move Note", - description: `Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. + description: `Move or rename a note and rewrite every link across the vault that points to it, like Obsidian's built-in rename. Incoming links in other notes — [[wikilinks]], [[wikilink|aliases]], [[wikilink#headings]], ![[embeds]], [markdown](links.md), and frontmatter links (e.g. related:) — and Canvas cards that embed the note are updated to the new path; the moved note's own relative links are fixed so they still resolve from the new folder, including relative links to attachments (e.g. ![[../assets/photo.png]], ![img](../assets/photo.png)). A link is only rewritten when leaving it unchanged would break it, so a short [[Note]] that stays unambiguous after a folder move is left alone. Example: vault_move_note({ old_path: "Inbox/Draft.md", new_path: "Inbox/Spec.md" }) — pure rename. Example: vault_move_note({ old_path: "Inbox/spec.md", new_path: "Inbox/Spec.md" }) — case-only rename; works even where the filesystem treats both spellings as one file. @@ -1224,25 +1280,23 @@ Parameters: - prune_empty_folders keeps a folder that holds any file, even a hidden .DS_Store. An in-place rename or a move into a subfolder of the source prunes nothing. Pruning is best-effort: a folder that can't be removed never fails the call. Errors: -- "destination exists: …" — a note already lives at new_path; this tool never overwrites. Pick a free path or delete the existing note first. +- "destination exists: …" — this tool never overwrites; pick a free new_path or delete the existing note first. - "note not found: …" — old_path does not exist; ${noteNotFoundVerifyRemedy}. -- "source and destination are the same path" — old_path and new_path name the same note, so there is nothing to move. -- "cannot move protected path …" / "cannot move into protected path …" — old_path or new_path sits under a protected folder (listed under When to use); pick a new_path outside it, or leave a protected note where it is. -- "cannot read daily notes config from .obsidian/daily-notes.json" — the file exists but is unreadable; repair it, or set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry. -- "path must end in …" — both old_path and new_path must end in .md. +- "source and destination are the same path" — nothing to move; pass a different new_path. +- "cannot move protected path …" / "cannot move into protected path …" — old_path or new_path sits under a protected folder; pick a new_path outside it, or leave a protected note where it is. +- "cannot read daily notes config from .obsidian/daily-notes.json" — ask the vault's owner to repair it, or the server operator to set DAILY_NOTES_FOLDER or PROTECTED_PATHS, then retry. +${NOTE_EXTENSION_ERROR_ENTRY}. - "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use vault-relative paths with no hidden (dot-prefixed) file or folder in them. - "concurrent write in progress" — a write is in flight on the note, the destination, or one of its backlink sources; retry the move. -- "backlink set did not stabilize" — the vault was modified during the move and new backlink sources kept appearing across retries; nothing was written; retry the move. -- "move incomplete: …" — an ordinary move failed partway (rare: a permission or disk error); no data is lost, and the error names what failed and the resulting state. If a backlink write failed: new_path exists and old_path is intact, so delete the partial new_path, then re-run the move. If the final delete failed: both paths exist, so delete old_path to finish. -- A case-only rename that fails partway — "move aborted: could not rename to …" means nothing was written; retry the move, and if it fails again ask the vault's owner to fix the folder's permissions. "move incomplete: …" means the note already lives at new_path and old_path is gone, so fix the remaining links in place (the error names the note whose update failed) instead of re-running the move. -- "move aborted: could not read …" / "move aborted: could not rewrite …" — the note, or a note linking to it, could not be read, or could not be saved with its links rewritten (the error says why and how to fix it); nothing was written; fix that note, then retry. -- "move aborted: could not write to …" — new_path could not be created; nothing was written. The folder or disk can't be written; ask the vault's owner to fix it. +- "backlink set did not stabilize" — notes linking to the note kept changing during the move; nothing was written; retry the move. +- "move aborted: …" — nothing was written. "could not read …" names the note, or a note linking to it, that could not be read; fix that note, then retry. "could not rewrite …" says why a note could not be saved with its links rewritten, and how to fix it. "could not write to …", or "could not rename to …" for a case-only rename, means new_path could not be created (new parent folders it needed may remain); retry once, then ask the vault's owner to fix the folder or disk. +- "move incomplete: …" — the move failed partway; no data is lost, and the message names what failed, the state it left, and the step that finishes the move. ${FILESYSTEM_ERROR_ENTRY}; nothing was written. -${INDEX_ENGINE_ERROR_ENTRY}; nothing was written. +- "[SqliteError]: …" — the search index failed and nothing was written; the message says what to do. Obsidian syntax: Link rewrites preserve each link's existing form — embed marker (!), heading anchor (#…), and alias (|…) are kept; a markdown link keeps its original extension and link text. -Returns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed — 0 unless prune_empty_folders was set).`, +Returns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten, including the moved note's own relative links), updated_notes (sorted paths of the other notes and Canvas files that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of empty parent folders removed).`, inputSchema: { old_path: z .string() @@ -1254,7 +1308,7 @@ Returns: JSON with moved_to (the new path), links_updated (count of link occurre .string() .min(1) .describe( - 'Destination vault-relative path (e.g. "Projects/Spec.md"). Must end in .md and must not already exist; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder.', + 'Destination vault-relative path (e.g. "Projects/Spec.md"). Must end in .md; parent folders are created as needed. Use the exact letter case of existing folders; a different case can create a second folder.', ), prune_empty_folders: z .boolean() @@ -1322,11 +1376,15 @@ Returns: JSON with moved_to (the new path), links_updated (count of link occurre }, ) + const updatePropertiesNoteNotFoundRemedy = isToolEnabled("vault_write_note") + ? "create the note first with vault_write_note" + : noteNotFoundCheckRemedy + registerTool( TOOL_NAMES.VAULT_UPDATE_PROPERTIES, { title: "Update Properties", - description: `Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (null writes an empty property, {} removes it). A merge refuses a block it cannot keep — invalid YAML, a list, a single value, or a YAML tag — so replace is how to repair one. The body is kept; a merge only adds a missing final newline. + description: `Update a note's frontmatter properties. By default they merge shallowly — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. With replace: true they replace the whole properties block (here null writes an empty property, and {} removes it). The body is kept; a merge only adds a missing final newline. Example: vault_update_properties({ path: "Projects/todo.md", properties: { status: "active", draft: null } }) Example: vault_update_properties({ path: "Projects/todo.md", properties: { title: "Todo", status: "active" }, replace: true }) @@ -1335,12 +1393,12 @@ When to use: Changing tags, status, type, or any property without reading/rewrit ${whenToolEnabledText("vault_write_note", "Prefer vault_write_note when creating a new note, or replacing the body (with overwrite: true). ")}${whenToolEnabledText("vault_read_note", "Read current properties first with vault_read_note({ properties_only: true }), or the full note when repairing a block. ")}Arrays are replaced entirely, not appended to. Errors: -- "note not found" — path does not exist; create the note first with vault_write_note -- "path must end in …" — add the .md extension -- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it -- "concurrent write in progress" — another write to this note is in flight; re-read the note and retry +- "note not found" — path does not exist; ${updatePropertiesNoteNotFoundRemedy} +${NOTE_EXTENSION_ERROR_ENTRY} +${PATH_SAFETY_ERROR_ENTRY} +${CONCURRENT_WRITE_REREAD_ERROR_ENTRY} ${FILESYSTEM_ERROR_ENTRY} -${describePropertiesBlockErrorEntry("without replace: true")} +- "properties block …" — without replace: true, the merge refuses a block it cannot keep: invalid YAML, a list, a single value, or a YAML tag; the error says how to repair it ${OPENING_BLOCK_ERROR_ENTRY} Obsidian syntax: Use arrays for multi-value fields (tags: [a, b]), quote wikilinks ("[[Note]]"), keep types consistent (mismatches cause silent query failures). diff --git a/src/vault-mcp/search/__tests__/search-index.test.ts b/src/vault-mcp/search/__tests__/search-index.test.ts index dbc27236c..fe47b14a7 100644 --- a/src/vault-mcp/search/__tests__/search-index.test.ts +++ b/src/vault-mcp/search/__tests__/search-index.test.ts @@ -4874,6 +4874,20 @@ describe("findOrphans", () => { }) }) + it("accepts up to 994 excluded folders and refuses 995", () => { + const queryIndex = createSearchIndex(":memory:") + const createExcludeFolders = (count: number): string[] => { + return Array.from({ length: count }, (_, folderIndex) => `Folder${folderIndex}`) + } + const largestAccepted = createExcludeFolders(994) + const oneTooMany = createExcludeFolders(995) + + expect(queryIndex.findOrphans({ excludeFolders: largestAccepted }, logger)).toEqual([]) + expect(() => { + queryIndex.findOrphans({ excludeFolders: oneTooMany }, logger) + }).toThrow("too many excluded folders") + }) + it("propagates unrelated SQLite query failures unchanged", () => { const queryIndex = createSearchIndex(":memory:") const requestLogger = { ...logger, warn: vi.fn() } @@ -7237,6 +7251,39 @@ describe("canvas file content and links", () => { }) }) + it("reports a file's full parent folder and a note's top-level folder", async () => { + const fileIndex = createSearchIndex(":memory:", undefined, undefined, { + fileToolsEnabled: true, + }) + fileIndex.upsertNote( + { + filePath: "Diagrams/System/plan.md", + rawContent: "# Plan\n\nThe architecture deployment plan.\n", + fileStat: testStat(1000), + }, + logger, + ) + fileIndex.upsertNonMdFile("Diagrams/System/arch.canvas", 500) + fileIndex.upsertFileContent( + { + filePath: "Diagrams/System/arch.canvas", + rawContent: CANVAS_WITH_FILE_NODES, + fileStat: testStat(5000, 500), + }, + logger, + ) + + const { results } = await fileIndex.hybridSearch({ query: "architecture deployment" }, logger) + const foldersByPath = results + .map(({ path, folder }) => ({ path, folder })) + .toSorted((left, right) => left.path.localeCompare(right.path)) + + expect(foldersByPath).toEqual([ + { path: "Diagrams/System/arch.canvas", folder: "Diagrams/System" }, + { path: "Diagrams/System/plan.md", folder: "Diagrams" }, + ]) + }) + it("canvas file-node references appear as backlinks", () => { const fileIndex = createSearchIndex(":memory:", undefined, undefined, { fileToolsEnabled: true, @@ -7259,7 +7306,7 @@ describe("canvas file content and links", () => { logger, ) const backlinks = fileIndex.getBacklinks({ path: "Projects/vault-cortex.md" }, logger) - expect(backlinks.map((backlink) => backlink.path)).toEqual(["Diagrams/arch.canvas"]) + expect(backlinks).toEqual([{ path: "Diagrams/arch.canvas", title: "arch", bytes: 500 }]) }) it("canvas outgoing links show file-node targets", () => { diff --git a/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts b/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts index c7eca56f3..901f5dcc6 100644 --- a/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts @@ -2223,6 +2223,11 @@ describe("listAssets", () => { expect(files).toEqual(["assets/board.canvas", "assets/photo.png"]) }) + it("returns an empty listing when folder names a file", async () => { + const files = await listAssets({ vaultPath: vault, folder: "assets/photo.png" }, logger) + expect(files).toEqual([]) + }) + it("includes a symlinked asset in the listing", async () => { await symlink("assets/photo.png", join(vault, "linked.png")) const files = await listAssets({ vaultPath: vault }, logger)