From 445a36ae7d0bede7ee7d6529dab4bee227177a6a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:15:29 -0400 Subject: [PATCH 01/32] fix(tools): point Errors entries at messages that carry their remedy, and polish descriptions - Shorten Errors entries whose message already gives the fix, keeping each entry's reading and every state fact. - Merge vault_move_note's abort and partial-move entries, name the Canvas cards a move updates, and send its daily-notes config remedy to the owner. - Fix the six tools graded below 4.8: list_files, get_outgoing_links, search_by_folder, get_backlinks, move_note, list_memory_files. - Apply the second polish batch: lag lines, no-offset notes, null rules, tie-breaks, routing lines, and repeated facts cut. - Name the image-fit remedy MAX_IMAGE_OUTPUT_BYTES, and pin the 994-folder orphan exclusion limit with a test. - Share the memory-name Errors entry across the three memory tools. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/tool-surface/default.json | 74 ++++++++--------- .../tool-surface/disabled-tools.json | 72 ++++++++--------- .../tool-surface/embedding-off.json | 74 ++++++++--------- .../file-tools-off+embedding-off.json | 70 ++++++++-------- .../tool-surface/file-tools-off.json | 70 ++++++++-------- .../memory-off+embedding-off.json | 62 +++++++------- ...mory-off+file-tools-off+embedding-off.json | 58 ++++++------- .../memory-off+file-tools-off.json | 58 ++++++------- .../tool-surface/memory-off.json | 62 +++++++------- .../tool-surface/obsidian-sync.json | 74 ++++++++--------- .../tool-surface/readonly+embedding-off.json | 40 ++++----- ...readonly+file-tools-off+embedding-off.json | 36 ++++----- .../tool-surface/readonly+file-tools-off.json | 36 ++++----- .../readonly+memory-off+embedding-off.json | 32 ++++---- ...mory-off+file-tools-off+embedding-off.json | 28 +++---- .../readonly+memory-off+file-tools-off.json | 28 +++---- .../tool-surface/readonly+memory-off.json | 32 ++++---- .../__snapshots__/tool-surface/readonly.json | 40 ++++----- .../__tests__/tool-definitions.test.ts | 41 +++++++--- src/vault-mcp/mcp-core/tools/asset-tools.ts | 25 +++--- .../mcp-core/tools/daily-note-tools.ts | 6 +- src/vault-mcp/mcp-core/tools/memory-tools.ts | 60 +++++++------- src/vault-mcp/mcp-core/tools/search-tools.ts | 58 ++++++++----- src/vault-mcp/mcp-core/tools/task-tools.ts | 37 ++++----- .../mcp-core/tools/vault-crud-tools.ts | 81 +++++++++---------- .../search/__tests__/search-index.test.ts | 14 ++++ 26 files changed, 662 insertions(+), 606 deletions(-) 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..6cf55504d 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -381,7 +381,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +664,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1148,7 +1148,7 @@ "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,7 +1179,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1273,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..d59625fa3 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -383,7 +383,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +666,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1089,7 +1089,7 @@ "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,7 +1120,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1214,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..a2aa3bc10 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -383,7 +383,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +666,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1150,7 +1150,7 @@ "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,7 +1181,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1275,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..fd8aa6b5a 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -384,7 +384,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +625,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1064,7 +1064,7 @@ "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,7 +1095,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1189,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..e6bf6cc99 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -383,7 +383,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +624,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1063,7 +1063,7 @@ "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,7 +1094,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1188,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..0a9a2ae73 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +565,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1008,7 +1008,7 @@ "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,7 +1039,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1133,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..92e9f8362 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +524,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -922,7 +922,7 @@ "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,7 +953,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1047,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..b20090d3e 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +523,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -921,7 +921,7 @@ "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,7 +952,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1046,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..ebe698f70 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +564,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1007,7 +1007,7 @@ "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,7 +1038,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1132,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..d54d1ee44 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 (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\" — pass heading with the target lane\n- \"heading \"X\" not found; available: ...\" — 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 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\" — the 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 and why\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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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 — 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 whole notes outside About Me/, which it protects.\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 error lists the file's available 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, since identical lines make every anchor ambiguous) 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 (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 → ##
\".", "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). Pick a brief fragment — do not paste the whole block." }, "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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -383,7 +383,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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.\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). Pick a brief fragment — do not paste the whole line." }, "position": { "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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names, 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, 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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +666,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: …\" — 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; 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; 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 …\" — 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 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; retry once, then ask the vault's owner to fix the folder or disk.\n- \"move incomplete: …\" — the move failed partway (rare: a permission or disk error); 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 before the move began, so 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 — 0 unless prune_empty_folders was set).", "inputSchema": { "type": "object", "properties": { @@ -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, 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\" — 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. 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.", "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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -1150,7 +1150,7 @@ "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,7 +1181,7 @@ { "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": { @@ -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=\"\"). 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\" — 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 \" — N is the number of matches replaced.", "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: 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 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,10 +1275,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). Pick a brief fragment — do not paste the whole block." }, "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 }, @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { @@ -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/. 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 with vault_delete_memory 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\" — the message names the two fixes.\n- \"section must be a single line\" — the message names the fix.\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\" — 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 …\" — 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.\"", "inputSchema": { "type": "object", "properties": { @@ -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 (null writes an empty property, {} 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\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.\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 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: …\" — 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. 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.", "inputSchema": { "type": "object", "properties": { @@ -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_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, or a link to one, 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 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 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..058fc29da 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -115,7 +115,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names. To find entries by topic instead, use vault_memory_recall.\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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +343,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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": { @@ -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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -726,7 +726,7 @@ "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,7 +757,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..a42242278 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -116,7 +116,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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. 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; it gives the valid file and section names. To find entries by topic instead, use vault_memory_recall.\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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +302,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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": { @@ -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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -640,7 +640,7 @@ "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,7 +671,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..1e419ee85 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -115,7 +115,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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. 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; it gives the valid file and section names. To find entries by topic instead, use vault_memory_recall.\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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +301,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -639,7 +639,7 @@ "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,7 +670,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..66f05eecf 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +289,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -631,7 +631,7 @@ "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,7 +662,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..d0f6e0f16 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +248,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -545,7 +545,7 @@ "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,7 +576,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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: …\" (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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..0efe24c93 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +247,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -544,7 +544,7 @@ "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,7 +575,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..1f1d75328 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +288,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -630,7 +630,7 @@ "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,7 +661,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..f2b5844eb 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 — 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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\" — exclude_folders has more than 994 entries; pass a shorter 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 (null when missing or not an ISO date), modified, bytes, leading_callout, additional_properties), sorted by most recently modified, then by path. 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 is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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: 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); 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. 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: only the entries dated on or after that 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. 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 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 }.", "inputSchema": { "type": "object", "properties": { @@ -114,7 +114,7 @@ "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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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 is looked up in the search index, so 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 — 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 the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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 starts at the filesystem root, escapes the vault (e.g. \"../elsewhere\") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", "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. 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; it gives the valid file and section names. To find entries by topic instead, use vault_memory_recall.\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 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, 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": { @@ -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 — it supports hierarchical prefix matching (\"project\" matches \"project/*\"). To count tags in one folder, or only the most-used ones, use vault_list_property_values({ key: \"tags\" }) with folder or limit; it counts every value of the tags property and lists spellings that differ in letter case apart.\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,7 +342,7 @@ { "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, ⏳ 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- sort_by: a task with none of a date sort's dates comes last; \"priority\" ascending puts highest first, with no priority 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, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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: [], total: 0, truncated: false, search_mode, reranked }, 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.", "inputSchema": { "type": "object", "properties": { @@ -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 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\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; 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 it has no pages or none 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 into …\" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it\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- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it\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: \"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\" — 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\" — 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 and limit page only a full read or a heading section\n- \"start line past the end\" — the message states the line count\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 (ISO 8601); 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 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": { @@ -725,7 +725,7 @@ "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,7 +756,7 @@ { "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": { @@ -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 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\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 { results: [], total: 0, search_mode, reranked }, 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; a total equal to limit may mean more exist, so raise limit to check), 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.", "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: exactly limit results may mean more exist, so raise limit to see them.\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": { @@ -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, \"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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.\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 the first key spelled tags in any letter case.", "inputSchema": { "type": "object", "properties": { 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..48ff8f17e 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -222,7 +222,7 @@ describe("registerTools", () => { // 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.", ) }) @@ -316,7 +316,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, since identical lines make every anchor ambiguous) or a manual edit, then retry.', ) }) @@ -2019,11 +2019,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 by most recently modified. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them.", ].join("\n"), ) expect(toolConfig.inputSchema?.shape.exclude_folders?.description).toBe( @@ -2964,7 +2964,9 @@ 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(description).toContain( + "BEFORE calling vault_get_memory; it gives the valid file and section names.", + ) expect(description).not.toContain("vault_update_memory") expect(description).not.toContain("pruning entries") }) @@ -2982,7 +2984,7 @@ 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", + "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") }) @@ -3008,6 +3010,25 @@ describe("DISABLED_TOOLS", () => { 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 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("names only the surviving consumer when vault_get_memory alone is disabled", () => { const registeredCalls = registerWithConfig({ DISABLED_TOOLS: "vault_get_memory", @@ -3296,21 +3317,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", @@ -3412,7 +3433,7 @@ describe("DISABLED_TOOLS", () => { }) 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." + "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 = diff --git a/src/vault-mcp/mcp-core/tools/asset-tools.ts b/src/vault-mcp/mcp-core/tools/asset-tools.ts index 03ee493dd..3c6d4d52c 100644 --- a/src/vault-mcp/mcp-core/tools/asset-tools.ts +++ b/src/vault-mcp/mcp-core/tools/asset-tools.ts @@ -80,6 +80,7 @@ export const registerAssetTools = ({ registerTool, safeHandler, safeHandlerContent, + whenToolEnabledText, vaultPath, logger: sessionLogger, config, @@ -96,6 +97,8 @@ 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. 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. + 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. @@ -108,12 +111,10 @@ raw: true switches a canvas or PDF to its other form: 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. - 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) +- "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it - "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 @@ -123,12 +124,13 @@ Errors: - "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 page rendering failed" — raw: true was set and the PDF opened, but it has no pages or none 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 into …" — even shrunk to 64 pixels on its longer side, the image is over the image output budget, which happens only when the server's owner sets MAX_IMAGE_OUTPUT_BYTES very low; ask them to raise it ${FILESYSTEM_ERROR_ENTRY} - "file changed while reading" — the file was written while it was read (for example by Sync); 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 +- Any other error while reading a PDF or image comes back in the reading library's own words; treat the file as damaged and replace or re-export it Returns: 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: { @@ -227,18 +229,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")}. 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. 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 starts at the filesystem root, escapes the vault (e.g. "../elsewhere") or 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; a large one comes back shrunk, so it is still cheap to read), .canvas, .pdf, and the text formats .svg/.json/.txt/.csv/.xml/.log/.yaml/.yml/.base. It cannot read the other listed types.", + )}`, 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..7fc69d121 100644 --- a/src/vault-mcp/mcp-core/tools/daily-note-tools.ts +++ b/src/vault-mcp/mcp-core/tools/daily-note-tools.ts @@ -24,7 +24,7 @@ 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. @@ -32,11 +32,11 @@ Parameters: 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 +- "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); 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).${isToolEnabled("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..6e241968a 100644 --- a/src/vault-mcp/mcp-core/tools/memory-tools.ts +++ b/src/vault-mcp/mcp-core/tools/memory-tools.ts @@ -11,6 +11,11 @@ import { 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. */ @@ -56,8 +61,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}; it gives the valid file and section names` + : "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 @@ -76,7 +81,7 @@ export const registerMemoryTools = ({ 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. -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 (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. Example: vault_get_memory({ file: "Principles" }) — the whole file Example: vault_get_memory({ file: "Principles", section: "Decision heuristics (newest first)" }) @@ -84,12 +89,12 @@ Example: vault_get_memory({ file: "Opinions", section: "Code patterns", on_or_af 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. +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.")} 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 requires a file" / "on_or_after requires a file" — add file +- "memory file not found" — call vault_list_memory_files to discover valid names +${MEMORY_FILE_NAME_ERROR_ENTRY} - "section not found: …" — no H2 heading matches; the error lists the file's available sections - "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 @@ -110,11 +115,7 @@ Returns: Without on_or_after, raw markdown text. With on_or_after, JSON { entrie .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.', ), - 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) => { @@ -179,16 +180,16 @@ Returns: Without on_or_after, raw markdown text. With on_or_after, JSON { entrie 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 }] }, ...] -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. 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, 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) => { @@ -215,8 +216,10 @@ 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. +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 +- No matching entries returns { entries: [], total: 0, truncated: false, search_mode, reranked }, not an error - An unknown file returns empty results — call vault_list_memory_files to discover valid names ${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.` @@ -297,7 +300,7 @@ ${recallReturnsLead} In keyword-only mode search_mode is always "fts" and rerank 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}/. 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${whenToolEnabledText("vault_delete_memory", " with vault_delete_memory")} is expected maintenance rather than a violation. 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 @@ -311,11 +314,11 @@ 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. +- "entry must be a single line" — the message names the two fixes. +- "section must be a single line" — the message names the fix. - "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. +- "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 …" — 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} ${MEMORY_FILE_IS_FOLDER_ERROR_ENTRY} @@ -410,20 +413,19 @@ Example: vault_delete_memory({ file: "Opinions", section: "Tools and workflows ( 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. - +${whenToolEnabledText("vault_delete_note", `Prefer vault_delete_note for deleting whole notes outside ${config.memoryDir}/, which it protects.\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" — call vault_list_memory_files to discover valid names. +${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. +- "no entry matching …" — verify the exact text with vault_get_memory(file, section). +- "ambiguous: N entries match …" — remove the extra copy with ${whenToolEnabledText("vault_delete_span", "vault_delete_span (first_match: true, since identical lines make every anchor ambiguous) or ")}a manual edit, then retry. +- "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 ${whenToolEnabledText("vault_delete_span", "vault_delete_span or ")}a manual edit. ${MEMORY_FILE_READ_ERROR_ENTRY} ${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..04c7d5f10 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -70,10 +70,12 @@ 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 { results: [], total: 0, search_mode, reranked }, 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 @@ -95,9 +97,11 @@ ${searchSharedExamples} When to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Try natural-language queries, not just keywords. ${searchAlternativesLine} +${searchLagLine} + ${searchErrorsSection} -Returns: JSON with results array (${searchResultFieldsText}), 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. ${searchResultDetailsText}` +Returns: JSON with results array (${searchResultFieldsText}), total (results returned, not all matches; a total equal to limit may mean more exist, so raise limit to check), 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. ${searchResultDetailsText}` : `Full-text search across ${searchedContentText}, ranked by relevance. ${searchSyntaxText} ${searchFiltersSection} @@ -108,9 +112,11 @@ Example: vault_search({ query: "deployment", filters: { properties: { status: "a When to use: The primary discovery tool for content-based queries, optionally constrained by metadata. ${searchAlternativesLine} +${searchLagLine} + ${searchErrorsSection} -Returns: JSON with results array (${searchResultFieldsText}), 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. ${searchResultDetailsText}`, +Returns: JSON with results array (${searchResultFieldsText}), total (results returned, not all matches; a total equal to limit may mean more exist, so raise limit to check), search_mode ("fts" — keyword-only ranking), and reranked (always false in keyword-only mode). A higher score is more relevant. ${searchResultDetailsText}`, inputSchema: { query: z.string().min(1).describe("Search query text"), filters: createStrictInputObject({ @@ -267,7 +273,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/*"). +Prefer vault_search_by_tag once you know which tag to query — it supports hierarchical prefix matching ("project" matches "project/*").${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; it counts every value of the tags property and lists spellings that differ in letter case apart.')} Errors: - A vault with no tagged notes returns an empty array, not an error. @@ -309,6 +315,8 @@ 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} @@ -360,7 +368,7 @@ Prefer vault_list_notes when you only need paths. Prefer vault_search when you h 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: exactly limit results may mean more exist, so raise limit to see them. 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 +376,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() @@ -521,13 +529,16 @@ Parameters: - 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"]. - 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. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them. + +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.`, inputSchema: { @@ -560,15 +571,16 @@ 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" registerTool( @@ -583,11 +595,14 @@ 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. -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. +Parameters: +- path is the target's full vault-relative 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, then path; title is a canvas source's file name without .canvas; 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}. +- 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 @@ -624,7 +639,7 @@ ${INDEX_ENGINE_ERROR_ENTRY}`, 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 (vault-relative paths embedded in the canvas), 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 up to any #heading or |alias, in its original letter case, 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" }) @@ -632,11 +647,14 @@ 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. +Parameters: +- path is looked up in the search index, so 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}. 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. +- A path the index does not hold (missing, in a dot-prefixed folder, or not indexed yet) 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 @@ -708,14 +726,16 @@ Prefer vault_get_backlinks to check the connectivity of one specific note rather 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 by most recently modified. There is no offset, and nothing in the response signals truncation: exactly limit results may mean more exist, so raise limit to see them. + +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" — exclude_folders has more than 994 entries; pass a shorter list, 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, then by path. 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..6403e27a6 100644 --- a/src/vault-mcp/mcp-core/tools/task-tools.ts +++ b/src/vault-mcp/mcp-core/tools/task-tools.ts @@ -43,11 +43,12 @@ 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, with no priority 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 +- "path must end in …" — add the ".md" extension - "tag must not be empty …" — tag is "#" alone; pass the tag name - No matches returns { total: 0, tasks: [] }, not an error ${INDEX_ERROR_ENTRY} @@ -243,20 +244,20 @@ 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 +- "heading required for Kanban boards" — pass heading with the target lane +- "heading "X" not found; available: ..." — the error lists the note's headings - "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" — the 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 and why - "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 +- "taskId ... contains invalid characters" / "dependsOn entry ... contains invalid characters" — the message names the allowed characters +- "unrecognized recurrence rule ..." — the message gives example rules +- "invalid date: …" — the message names the expected format - "concurrent write in progress" — another write to this note is in flight; retry ${FILESYSTEM_ERROR_ENTRY} ${describePropertiesBlockErrorEntry()} @@ -481,7 +482,7 @@ Parameters: - 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. 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. @@ -490,23 +491,23 @@ 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 +- "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 +- "heading "X" not found; available: ..." — 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 - "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 +- "invalid date: …" — the message names the expected format - "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" +- "taskId ... contains invalid characters" / "dependsOn entry ... contains invalid characters" — the message names the allowed characters +- "unrecognized recurrence rule ..." — the message gives example rules - "concurrent write in progress" — another write to this note is in flight; retry ${FILESYSTEM_ERROR_ENTRY} ${describePropertiesBlockErrorEntry()} 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..db111cca7 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -15,7 +15,6 @@ import { TOOL_NAMES } from "../tool-registry.js" import type { ToolRegistrationContext } from "./tool-helpers.js" import { FILESYSTEM_ERROR_ENTRY, - INDEX_ENGINE_ERROR_ENTRY, LISTING_FILESYSTEM_ERROR_ENTRY, describePropertiesBlockErrorEntry, describeTextWindow, @@ -81,7 +80,7 @@ export const registerVaultCrudTools = ({ 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 }) @@ -97,13 +96,13 @@ Section boundaries: a section spans from its heading to the next heading of the 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 +- "heading not found" — the message lists the available headings - "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 +- "outline, heading, and properties_only are mutually exclusive — set at most one" - "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 +- "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 - '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 - "properties block is not valid YAML …" — a full read, outline and heading still work; the error says how to repair it @@ -111,7 +110,7 @@ ${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 on-disk size and modified is its filesystem modification time (ISO 8601); 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 or the full note${whenToolEnabledText("vault_replace_in_note", ", and edit via vault_replace_in_note")}.`, inputSchema: { path: z .string() @@ -127,7 +126,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() @@ -148,9 +147,7 @@ Outline: bytes at the root is the whole file's on-disk size and modified is its .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() @@ -311,7 +308,7 @@ When to use: Browsing what exists in a folder by filename, or finding notes matc 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. 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 +318,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() @@ -379,23 +376,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 -- "cannot write note …: that path is not a file" — a folder already has this name; choose another path +- "cannot write note …: that path is not a file" — a folder, or a link to one, 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 +- "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 +- "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 can't be kept as properties; the error says how to avoid it 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). @@ -527,7 +524,7 @@ Section boundaries: a section spans from its heading to the next heading of the 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 +- "heading not found" — the message lists the available headings - "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 @@ -535,7 +532,7 @@ Errors: - "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 +- "content contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -665,7 +662,7 @@ Errors: - "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 +- "new_text contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -762,14 +759,14 @@ 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} +- "start anchor not found" / "end anchor not found" — no body line holds the fragment where it is searched; ${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 @@ -788,14 +785,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). Pick a brief fragment — do not paste the whole block.", ), 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 +858,17 @@ 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")} +- 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: 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. 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} +- "start anchor not found" / "end anchor not found" — no body line holds the fragment where it is searched; ${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 +- "content contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -889,14 +886,14 @@ 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). Pick a brief fragment — do not paste the whole block.", ), 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.", + "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() @@ -974,11 +971,11 @@ Parameters: Errors: - "note not found" — ${noteNotFoundVerifyRemedy} - "path must end in …" — add the .md extension -- "anchor not found" — fragment not on any line; ${anchorNotFoundRemedy} +- "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 +- "content contains a control character" — remove the character the message names ${FILESYSTEM_ERROR_ENTRY} ${OPENING_BLOCK_ERROR_ENTRY} @@ -996,7 +993,7 @@ Returns: Confirmation message "Inserted line(s) anchor in