From 23006e260691c280a61f75ac69f6303e839f62e0 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:24:09 -0400 Subject: [PATCH 1/3] test(mcp-core): add tool-surface snapshot baseline + drift test Co-Authored-By: Claude Fable 5 --- .gitattributes | 5 + .prettierignore | 3 + package.json | 1 + .../__snapshots__/tool-surface/default.json | 1843 ++++++++++++++++ .../tool-surface/disabled-tools.json | 1785 ++++++++++++++++ .../tool-surface/embedding-off.json | 1845 +++++++++++++++++ .../file-tools-off+embedding-off.json | 1763 ++++++++++++++++ .../tool-surface/file-tools-off.json | 1762 ++++++++++++++++ .../memory-off+embedding-off.json | 1639 +++++++++++++++ ...mory-off+file-tools-off+embedding-off.json | 1557 ++++++++++++++ .../memory-off+file-tools-off.json | 1556 ++++++++++++++ .../tool-surface/memory-off.json | 1638 +++++++++++++++ .../tool-surface/readonly+embedding-off.json | 1017 +++++++++ ...readonly+file-tools-off+embedding-off.json | 935 +++++++++ .../tool-surface/readonly+file-tools-off.json | 934 +++++++++ .../readonly+memory-off+embedding-off.json | 932 +++++++++ ...mory-off+file-tools-off+embedding-off.json | 850 ++++++++ .../readonly+memory-off+file-tools-off.json | 849 ++++++++ .../tool-surface/readonly+memory-off.json | 931 +++++++++ .../__snapshots__/tool-surface/readonly.json | 1016 +++++++++ .../__tests__/tool-surface-capture.ts | 178 ++ .../__tests__/tool-surface-snapshot.test.ts | 41 + src/vault-mcp/mcp-core/mcp-router.ts | 2 +- 23 files changed, 23081 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json create mode 100644 src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json create mode 100644 src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts create mode 100644 src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c6dd7f35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Multi-flip combo snapshots (two or more config flags off-default — "+" in +# the filename) repeat text already visible in the default, single-flip, and +# disabled-tools files, so GitHub collapses their PR diffs. The reviewable +# copies stay expanded. +src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/*+*.json linguist-generated=true diff --git a/.prettierignore b/.prettierignore index b295c334..6500c611 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,9 @@ node_modules/ # Fixture vault content is byte-exact test data — reformatting it changes # what the integration tests exercise src/__tests__/integration/fixtures/vault/ +# Tool-surface baseline files are byte-exact snapshots written by vitest — +# reformatting them fails the drift test +src/vault-mcp/mcp-core/__tests__/__snapshots__/ *.db *.sqlite CHANGELOG.md diff --git a/package.json b/package.json index 68239894..9d992c11 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "markdownlint": "markdownlint-cli2 \"**/*.md\"", "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\"", "test": "vitest run", + "snapshot:update": "vitest run -u src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts", "test:coverage": "vitest run --coverage", "test:watch": "vitest", "test:cli-pty": "vitest run --config vitest.cli-pty.config.ts", 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 new file mode 100644 index 00000000..1fc4a997 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json @@ -0,0 +1,1843 @@ +{ + "env": {}, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from About Me/ files. Use vault_write_note and vault_update_memory for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_memory", + "title": "Delete Memory Entry", + "description": "Delete a single dated entry from a About Me/ memory file. Both date and entry text are required for exact matching — ensures only the intended entry is removed.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"AI tooling & memory (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\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: when a preference or fact has since evolved, append the new state via vault_update_memory (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_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together uniquely identify the bullet line within the given section. If multiple entries share the same date and text, deletion fails as ambiguous.\n- section scopes the match — an identical entry under a different heading is not found. Section matching is case-insensitive, with or without the \"(newest first)\" suffix.\n\nErrors:\n- \"memory file must not start with a dot\" — a dot-prefixed name would target a hidden file; memory files are always visible notes.\n- \"date must be a real ISO calendar date\" — date only accepts an existing calendar date in bare YYYY-MM-DD form. A hand-edited bullet carrying an impossible date cannot be targeted by this tool — remove it with vault_delete_span or a manual edit.\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 (e.g. from hand edits, sync conflicts, or entries predating duplicate protection; vault_update_memory refuses to write exact duplicates). 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 before retrying.\n\nReturns: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading containing the entry. Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "date": { + "type": "string", + "minLength": 1, + "description": "ISO YYYY-MM-DD date of the entry (e.g. \"2026-05-01\"). Must match the date shown by vault_get_memory." + }, + "entry": { + "type": "string", + "description": "Exact entry text as shown by vault_get_memory — without the \"- **YYYY-MM-DD**: \" prefix or bullet. Both date and entry must match for deletion." + } + }, + "required": [ + "file", + "section", + "date", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every relevant dated entry 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\") whose relevance cut would come back empty 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range — so raise max_results or narrow the query for the complete set. search_mode is \"hybrid\" when vector matching contributed, \"fts\" when the entries came from keyword matching alone — including the any-term fallback that rescues a would-be-empty result; reranked is true when the cross-encoder relevance cut was applied.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — natural language works best (semantic matching bridges phrasing drift across months); content words about the topic rank better than meta framing (\"testing philosophy\" over \"opinions on testing\")" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_memory", + "title": "Update Memory", + "description": "Append a dated entry to a section of a About Me/ memory file. The server prefixes the date automatically (\"- **YYYY-MM-DD**: entry text\") and inserts newest-first by default. 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\" })\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). A new section name that is nearly identical to an existing heading (an HTML-entity slip, typo, or spacing variation) is rejected instead of created, so a mistyped name cannot silently fragment the file — names differing only in digits (e.g. \"2025\" vs \"2026\") are treated as distinct.\n\nParameters:\n- options.date — ISO YYYY-MM-DD, defaults to today (server timezone).\n- options.position — \"top\" (default, newest-first) inserts above existing entries; \"bottom\" appends below them.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"refusing memory write: … would shrink content\" — safety guard for diverged on-disk content. Re-read with vault_get_memory before retrying.\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/section contains a control character\" — entry or section includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" — a dot-prefixed name would create a hidden file (invisible in Obsidian and to every listing); choose a visible name.\n- \"section not created: … is nearly identical to existing section …\" — near-duplicate guard; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section.\n\nReturns: Confirmation message (notes when an identical entry already existed and nothing was written).", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "entry": { + "type": "string", + "minLength": 1, + "description": "Raw entry text — a single line (newlines are rejected); the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + }, + "options": { + "description": "Optional date and position overrides", + "type": "object", + "properties": { + "date": { + "description": "ISO YYYY-MM-DD date (defaults to today)", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Insert position (default \"top\" = newest first)", + "type": "string", + "enum": [ + "top", + "bottom" + ] + } + } + } + }, + "required": [ + "file", + "section", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "memory-review", + "title": "Reflect on memory (read as an evolution)", + "description": "Reflect on the About Me/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.", + "arguments": [ + { + "name": "file", + "description": "Memory file to review (e.g. one from About Me/); omit to review all", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..d1097624 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json @@ -0,0 +1,1785 @@ +{ + "env": { + "DISABLED_TOOLS": "vault_patch_note" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from About Me/ files. Use vault_write_note and vault_update_memory for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_memory", + "title": "Delete Memory Entry", + "description": "Delete a single dated entry from a About Me/ memory file. Both date and entry text are required for exact matching — ensures only the intended entry is removed.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"AI tooling & memory (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\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: when a preference or fact has since evolved, append the new state via vault_update_memory (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_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together uniquely identify the bullet line within the given section. If multiple entries share the same date and text, deletion fails as ambiguous.\n- section scopes the match — an identical entry under a different heading is not found. Section matching is case-insensitive, with or without the \"(newest first)\" suffix.\n\nErrors:\n- \"memory file must not start with a dot\" — a dot-prefixed name would target a hidden file; memory files are always visible notes.\n- \"date must be a real ISO calendar date\" — date only accepts an existing calendar date in bare YYYY-MM-DD form. A hand-edited bullet carrying an impossible date cannot be targeted by this tool — remove it with vault_delete_span or a manual edit.\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 (e.g. from hand edits, sync conflicts, or entries predating duplicate protection; vault_update_memory refuses to write exact duplicates). 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 before retrying.\n\nReturns: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading containing the entry. Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "date": { + "type": "string", + "minLength": 1, + "description": "ISO YYYY-MM-DD date of the entry (e.g. \"2026-05-01\"). Must match the date shown by vault_get_memory." + }, + "entry": { + "type": "string", + "description": "Exact entry text as shown by vault_get_memory — without the \"- **YYYY-MM-DD**: \" prefix or bullet. Both date and entry must match for deletion." + } + }, + "required": [ + "file", + "section", + "date", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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.\n Prefer vault_replace_span when replacing a block rather than inserting next to it.\n\nParameters:\n- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every relevant dated entry 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\") whose relevance cut would come back empty 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range — so raise max_results or narrow the query for the complete set. search_mode is \"hybrid\" when vector matching contributed, \"fts\" when the entries came from keyword matching alone — including the any-term fallback that rescues a would-be-empty result; reranked is true when the cross-encoder relevance cut was applied.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — natural language works best (semantic matching bridges phrasing drift across months); content words about the topic rank better than meta framing (\"testing philosophy\" over \"opinions on testing\")" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_memory", + "title": "Update Memory", + "description": "Append a dated entry to a section of a About Me/ memory file. The server prefixes the date automatically (\"- **YYYY-MM-DD**: entry text\") and inserts newest-first by default. 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\" })\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). A new section name that is nearly identical to an existing heading (an HTML-entity slip, typo, or spacing variation) is rejected instead of created, so a mistyped name cannot silently fragment the file — names differing only in digits (e.g. \"2025\" vs \"2026\") are treated as distinct.\n\nParameters:\n- options.date — ISO YYYY-MM-DD, defaults to today (server timezone).\n- options.position — \"top\" (default, newest-first) inserts above existing entries; \"bottom\" appends below them.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"refusing memory write: … would shrink content\" — safety guard for diverged on-disk content. Re-read with vault_get_memory before retrying.\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/section contains a control character\" — entry or section includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" — a dot-prefixed name would create a hidden file (invisible in Obsidian and to every listing); choose a visible name.\n- \"section not created: … is nearly identical to existing section …\" — near-duplicate guard; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section.\n\nReturns: Confirmation message (notes when an identical entry already existed and nothing was written).", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "entry": { + "type": "string", + "minLength": 1, + "description": "Raw entry text — a single line (newlines are rejected); the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + }, + "options": { + "description": "Optional date and position overrides", + "type": "object", + "properties": { + "date": { + "description": "ISO YYYY-MM-DD date (defaults to today)", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Insert position (default \"top\" = newest first)", + "type": "string", + "enum": [ + "top", + "bottom" + ] + } + } + } + }, + "required": [ + "file", + "section", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "memory-review", + "title": "Reflect on memory (read as an evolution)", + "description": "Reflect on the About Me/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.", + "arguments": [ + { + "name": "file", + "description": "Memory file to review (e.g. one from About Me/); omit to review all", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..e6aae774 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json @@ -0,0 +1,1845 @@ +{ + "env": { + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from About Me/ files. Use vault_write_note and vault_update_memory for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_memory", + "title": "Delete Memory Entry", + "description": "Delete a single dated entry from a About Me/ memory file. Both date and entry text are required for exact matching — ensures only the intended entry is removed.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"AI tooling & memory (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\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: when a preference or fact has since evolved, append the new state via vault_update_memory (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_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together uniquely identify the bullet line within the given section. If multiple entries share the same date and text, deletion fails as ambiguous.\n- section scopes the match — an identical entry under a different heading is not found. Section matching is case-insensitive, with or without the \"(newest first)\" suffix.\n\nErrors:\n- \"memory file must not start with a dot\" — a dot-prefixed name would target a hidden file; memory files are always visible notes.\n- \"date must be a real ISO calendar date\" — date only accepts an existing calendar date in bare YYYY-MM-DD form. A hand-edited bullet carrying an impossible date cannot be targeted by this tool — remove it with vault_delete_span or a manual edit.\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 (e.g. from hand edits, sync conflicts, or entries predating duplicate protection; vault_update_memory refuses to write exact duplicates). 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 before retrying.\n\nReturns: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading containing the entry. Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "date": { + "type": "string", + "minLength": 1, + "description": "ISO YYYY-MM-DD date of the entry (e.g. \"2026-05-01\"). Must match the date shown by vault_get_memory." + }, + "entry": { + "type": "string", + "description": "Exact entry text as shown by vault_get_memory — without the \"- **YYYY-MM-DD**: \" prefix or bullet. Both date and entry must match for deletion." + } + }, + "required": [ + "file", + "section", + "date", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every matching dated entry sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off — EMBEDDING_ENABLED=false), 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range. search_mode is always \"fts\" and reranked always false in keyword-only mode.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — use specific keywords (semantic matching is off; re-query with synonyms to cover vocabulary drift)" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_memory", + "title": "Update Memory", + "description": "Append a dated entry to a section of a About Me/ memory file. The server prefixes the date automatically (\"- **YYYY-MM-DD**: entry text\") and inserts newest-first by default. 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\" })\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). A new section name that is nearly identical to an existing heading (an HTML-entity slip, typo, or spacing variation) is rejected instead of created, so a mistyped name cannot silently fragment the file — names differing only in digits (e.g. \"2025\" vs \"2026\") are treated as distinct.\n\nParameters:\n- options.date — ISO YYYY-MM-DD, defaults to today (server timezone).\n- options.position — \"top\" (default, newest-first) inserts above existing entries; \"bottom\" appends below them.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"refusing memory write: … would shrink content\" — safety guard for diverged on-disk content. Re-read with vault_get_memory before retrying.\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/section contains a control character\" — entry or section includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" — a dot-prefixed name would create a hidden file (invisible in Obsidian and to every listing); choose a visible name.\n- \"section not created: … is nearly identical to existing section …\" — near-duplicate guard; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section.\n\nReturns: Confirmation message (notes when an identical entry already existed and nothing was written).", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "entry": { + "type": "string", + "minLength": 1, + "description": "Raw entry text — a single line (newlines are rejected); the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + }, + "options": { + "description": "Optional date and position overrides", + "type": "object", + "properties": { + "date": { + "description": "ISO YYYY-MM-DD date (defaults to today)", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Insert position (default \"top\" = newest first)", + "type": "string", + "enum": [ + "top", + "bottom" + ] + } + } + } + }, + "required": [ + "file", + "section", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "memory-review", + "title": "Reflect on memory (read as an evolution)", + "description": "Reflect on the About Me/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.", + "arguments": [ + { + "name": "file", + "description": "Memory file to review (e.g. one from About Me/); omit to review all", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..d35e92c4 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json @@ -0,0 +1,1763 @@ +{ + "env": { + "FILE_TOOLS_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_get_memory to retrieve user preferences and context from About Me/ files. Use vault_write_note and vault_update_memory for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_memory", + "title": "Delete Memory Entry", + "description": "Delete a single dated entry from a About Me/ memory file. Both date and entry text are required for exact matching — ensures only the intended entry is removed.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"AI tooling & memory (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\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: when a preference or fact has since evolved, append the new state via vault_update_memory (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_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together uniquely identify the bullet line within the given section. If multiple entries share the same date and text, deletion fails as ambiguous.\n- section scopes the match — an identical entry under a different heading is not found. Section matching is case-insensitive, with or without the \"(newest first)\" suffix.\n\nErrors:\n- \"memory file must not start with a dot\" — a dot-prefixed name would target a hidden file; memory files are always visible notes.\n- \"date must be a real ISO calendar date\" — date only accepts an existing calendar date in bare YYYY-MM-DD form. A hand-edited bullet carrying an impossible date cannot be targeted by this tool — remove it with vault_delete_span or a manual edit.\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 (e.g. from hand edits, sync conflicts, or entries predating duplicate protection; vault_update_memory refuses to write exact duplicates). 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 before retrying.\n\nReturns: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading containing the entry. Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "date": { + "type": "string", + "minLength": 1, + "description": "ISO YYYY-MM-DD date of the entry (e.g. \"2026-05-01\"). Must match the date shown by vault_get_memory." + }, + "entry": { + "type": "string", + "description": "Exact entry text as shown by vault_get_memory — without the \"- **YYYY-MM-DD**: \" prefix or bullet. Both date and entry must match for deletion." + } + }, + "required": [ + "file", + "section", + "date", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every matching dated entry sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off — EMBEDDING_ENABLED=false), 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range. search_mode is always \"fts\" and reranked always false in keyword-only mode.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — use specific keywords (semantic matching is off; re-query with synonyms to cover vocabulary drift)" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_memory", + "title": "Update Memory", + "description": "Append a dated entry to a section of a About Me/ memory file. The server prefixes the date automatically (\"- **YYYY-MM-DD**: entry text\") and inserts newest-first by default. 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\" })\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). A new section name that is nearly identical to an existing heading (an HTML-entity slip, typo, or spacing variation) is rejected instead of created, so a mistyped name cannot silently fragment the file — names differing only in digits (e.g. \"2025\" vs \"2026\") are treated as distinct.\n\nParameters:\n- options.date — ISO YYYY-MM-DD, defaults to today (server timezone).\n- options.position — \"top\" (default, newest-first) inserts above existing entries; \"bottom\" appends below them.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"refusing memory write: … would shrink content\" — safety guard for diverged on-disk content. Re-read with vault_get_memory before retrying.\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/section contains a control character\" — entry or section includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" — a dot-prefixed name would create a hidden file (invisible in Obsidian and to every listing); choose a visible name.\n- \"section not created: … is nearly identical to existing section …\" — near-duplicate guard; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section.\n\nReturns: Confirmation message (notes when an identical entry already existed and nothing was written).", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "entry": { + "type": "string", + "minLength": 1, + "description": "Raw entry text — a single line (newlines are rejected); the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + }, + "options": { + "description": "Optional date and position overrides", + "type": "object", + "properties": { + "date": { + "description": "ISO YYYY-MM-DD date (defaults to today)", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Insert position (default \"top\" = newest first)", + "type": "string", + "enum": [ + "top", + "bottom" + ] + } + } + } + }, + "required": [ + "file", + "section", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "memory-review", + "title": "Reflect on memory (read as an evolution)", + "description": "Reflect on the About Me/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.", + "arguments": [ + { + "name": "file", + "description": "Memory file to review (e.g. one from About Me/); omit to review all", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..5823558a --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json @@ -0,0 +1,1762 @@ +{ + "env": { + "FILE_TOOLS_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_get_memory to retrieve user preferences and context from About Me/ files. Use vault_write_note and vault_update_memory for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_memory", + "title": "Delete Memory Entry", + "description": "Delete a single dated entry from a About Me/ memory file. Both date and entry text are required for exact matching — ensures only the intended entry is removed.\n\nExample: vault_delete_memory({ file: \"Opinions\", section: \"AI tooling & memory (newest first)\", date: \"2026-05-01\", entry: \"Prefer X over Y\" })\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: when a preference or fact has since evolved, append the new state via vault_update_memory (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_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes.\n\nParameters:\n- date + entry together uniquely identify the bullet line within the given section. If multiple entries share the same date and text, deletion fails as ambiguous.\n- section scopes the match — an identical entry under a different heading is not found. Section matching is case-insensitive, with or without the \"(newest first)\" suffix.\n\nErrors:\n- \"memory file must not start with a dot\" — a dot-prefixed name would target a hidden file; memory files are always visible notes.\n- \"date must be a real ISO calendar date\" — date only accepts an existing calendar date in bare YYYY-MM-DD form. A hand-edited bullet carrying an impossible date cannot be targeted by this tool — remove it with vault_delete_span or a manual edit.\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 (e.g. from hand edits, sync conflicts, or entries predating duplicate protection; vault_update_memory refuses to write exact duplicates). 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 before retrying.\n\nReturns: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading containing the entry. Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "date": { + "type": "string", + "minLength": 1, + "description": "ISO YYYY-MM-DD date of the entry (e.g. \"2026-05-01\"). Must match the date shown by vault_get_memory." + }, + "entry": { + "type": "string", + "description": "Exact entry text as shown by vault_get_memory — without the \"- **YYYY-MM-DD**: \" prefix or bullet. Both date and entry must match for deletion." + } + }, + "required": [ + "file", + "section", + "date", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\nPrefer vault_delete_memory for removing individual dated entries from About Me/ memory files.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder; use vault_delete_memory for memory entries\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every relevant dated entry 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\") whose relevance cut would come back empty 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range — so raise max_results or narrow the query for the complete set. search_mode is \"hybrid\" when vector matching contributed, \"fts\" when the entries came from keyword matching alone — including the any-term fallback that rescues a would-be-empty result; reranked is true when the cross-encoder relevance cut was applied.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — natural language works best (semantic matching bridges phrasing drift across months); content words about the topic rank better than meta framing (\"testing philosophy\" over \"opinions on testing\")" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_memory", + "title": "Update Memory", + "description": "Append a dated entry to a section of a About Me/ memory file. The server prefixes the date automatically (\"- **YYYY-MM-DD**: entry text\") and inserts newest-first by default. 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\" })\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). A new section name that is nearly identical to an existing heading (an HTML-entity slip, typo, or spacing variation) is rejected instead of created, so a mistyped name cannot silently fragment the file — names differing only in digits (e.g. \"2025\" vs \"2026\") are treated as distinct.\n\nParameters:\n- options.date — ISO YYYY-MM-DD, defaults to today (server timezone).\n- options.position — \"top\" (default, newest-first) inserts above existing entries; \"bottom\" appends below them.\n\nObsidian syntax: Entry text is Obsidian Flavored Markdown. Watch for: #word = tag, [[ = wikilink. Escape with \\# or backticks when unintentional.\n\nErrors:\n- \"refusing memory write: … would shrink content\" — safety guard for diverged on-disk content. Re-read with vault_get_memory before retrying.\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/section contains a control character\" — entry or section includes a non-printable control byte; remove it before writing.\n- \"memory file must not start with a dot\" — a dot-prefixed name would create a hidden file (invisible in Obsidian and to every listing); choose a visible name.\n- \"section not created: … is nearly identical to existing section …\" — near-duplicate guard; pass the exact existing heading (listed in the error) to append there, or choose a clearly different name for a genuinely new section.\n\nReturns: Confirmation message (notes when an identical entry already existed and nothing was written).", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Memory file name without .md (e.g. \"Principles\")" + }, + "section": { + "type": "string", + "minLength": 1, + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix." + }, + "entry": { + "type": "string", + "minLength": 1, + "description": "Raw entry text — a single line (newlines are rejected); the server prepends \"- **YYYY-MM-DD**: \" automatically. Do not include the date or bullet prefix." + }, + "options": { + "description": "Optional date and position overrides", + "type": "object", + "properties": { + "date": { + "description": "ISO YYYY-MM-DD date (defaults to today)", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Insert position (default \"top\" = newest first)", + "type": "string", + "enum": [ + "top", + "bottom" + ] + } + } + } + }, + "required": [ + "file", + "section", + "entry" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "memory-review", + "title": "Reflect on memory (read as an evolution)", + "description": "Reflect on the About Me/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.", + "arguments": [ + { + "name": "file", + "description": "Memory file to review (e.g. one from About Me/); omit to review all", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..e51a9a8b --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json @@ -0,0 +1,1639 @@ +{ + "env": { + "MEMORY_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_write_note for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..fa0571b0 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json @@ -0,0 +1,1557 @@ +{ + "env": { + "MEMORY_ENABLED": "false", + "FILE_TOOLS_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_write_note for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..176a4b3b --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json @@ -0,0 +1,1556 @@ +{ + "env": { + "MEMORY_ENABLED": "false", + "FILE_TOOLS_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_write_note for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..7aa9c45e --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json @@ -0,0 +1,1638 @@ +{ + "env": { + "MEMORY_ENABLED": "false" + }, + "instructions": "Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_write_note for writes.\n\nVault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.", + "tools": [ + { + "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) with ➕ today auto-stamped — starting work is vault_update_task's job. Metadata is written in the format the vault's Tasks plugin is configured for (emoji unless the plugin config says Dataview).\n\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Fix login bug\", block_id: \"fix-login\", heading: \"Active\", priority: \"high\", due: \"2026-09-15\" })\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Ship the feature\", block_id: \"ship-feature\", heading: \"Up Next\", subtasks: [\"Design\", \"Implement\", \"Test\"] }) — card with checklist stages\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Sub-bug\", block_id: \"sub-bug\", parent_block_id: \"fix-login\", due: \"2026-09-01\" }) — full sub-task under a parent identified by block_id\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Quick fix\", block_id: \"quick-fix\", parent_line: 42 }) — sub-task under a parent identified by line number\nExample: vault_create_task({ path: \"TASKS.md\", description: \"Urgent fix\", block_id: \"urgent-fix\", heading: \"Active\", position: \"top\" }) — insert at the top of a lane instead of the default bottom\n\nWhen to use: Creating a new task card on a board or in a note. Guarantees correct field ordering (description → priority → ➕ created → 🛫 start → ⏳ scheduled → 📅 due → 🆔 task_id → ⛔ depends_on → ^block_id) 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- path (required): vault-relative path to the note (must end in \".md\"). The note must already exist.\n- description (required): the task text (before metadata fields).\n- block_id (required): the ^block-id for stable identification — letters, digits, and hyphens only. Must be unique within the note.\n- heading: target heading. Required on Kanban boards (notes with kanban-plugin frontmatter); optional on regular notes (omit to append at end of body).\n- parent_block_id / parent_line: the existing task to nest under as a sub-task, identified by its ^block-id or its 1-based line number — the same pair vault_update_task uses (block_id / line). Pass at most one. Either is mutually exclusive with heading — a sub-task lives wherever its parent lives.\n- position: \"top\" or \"bottom\" — where within the heading section the task is placed. Defaults to \"bottom\" (append). Kanban boards with new-card-insertion-method set to \"prepend\" default to \"top\" instead. Ignored when no heading or when placing under a parent.\n- priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\". Omit for normal priority (the plugin ranks \"no signifier\" between medium and low).\n- due / scheduled / start: YYYY-MM-DD dates (calendar-validated). Omit a date rather than guessing — an absent 📅 means \"no deadline\".\n- task_id: Tasks plugin 🆔 identifier for dependency chains.\n- depends_on: non-empty string array of Tasks plugin ⛔ dependency IDs (🆔 values of other tasks).\n- subtasks: string array of checklist item descriptions — created as indented [ ] lines under the card (no metadata, no block_ids). For full sub-tasks with their own dates, priority, and block_id, make a separate vault_create_task call with parent_block_id.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format (emoji when no plugin config is present).\n\nErrors:\n- \"note not found\" — path does not exist\n- \"heading required for Kanban boards\" — kanban-plugin note without heading\n- \"heading \"X\" not found; available: ...\" — no heading matches; the error lists the note's headings\n- \"parent task not found\" — parent_block_id or parent_line doesn't resolve to a task (message names the blockId or line tried)\n- \"parentBlockId and parentLine are mutually exclusive\" — both parent_block_id and parent_line were passed; drop one\n- \"parent and heading are mutually exclusive\" — a parent (parent_block_id or parent_line) and heading were both passed; drop one\n- \"blockId ... already exists in this note\" — pick a block_id not yet used in the note\n- \"blockId ... contains invalid characters\" — block_id must match [a-zA-Z0-9-]+\n- \"description is empty\" / \"dependsOn cannot be empty\" / \"subtasks cannot contain an empty item\" — whitespace-only description, an empty depends_on array, or a whitespace-only checklist item\n- \"description must be a single line\" / \"subtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"invalid date\" — a date param fails calendar validation\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the new card's 1-based position; 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 carry no block_id, so line is the handle for a follow-up update; changes lists every field written as \"field: before → after\", with \"(none)\" for an absent value.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note (must end in \".md\"). The note must already exist." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The task text (before metadata fields)." + }, + "block_id": { + "type": "string", + "minLength": 1, + "description": "The ^block-id for stable identification — letters, digits, and hyphens only ([a-zA-Z0-9-]+). Must be unique within the note." + }, + "heading": { + "description": "Target heading. Required on Kanban boards; optional on regular notes (omit to append at end of body).", + "type": "string", + "minLength": 1 + }, + "parent_block_id": { + "description": "^block-id (without the ^) of an existing task to nest under as a sub-task. Mutually exclusive with parent_line and heading.", + "type": "string", + "minLength": 1 + }, + "parent_line": { + "description": "1-based line number of an existing task to nest under as a sub-task. Mutually exclusive with parent_block_id and heading. Fragile if the file changed since the line was read.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "position": { + "description": "Where within the heading section the task is placed. Defaults to bottom. Kanban boards with new-card-insertion-method set to prepend default to top instead. Ignored when no heading or when placing under a parent.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "priority": { + "description": "Priority signifier (🔺⏫🔼🔽⏬). Omit for normal priority — no signifier is written.", + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + "due": { + "description": "Deadline (📅), YYYY-MM-DD, calendar-validated. Omit when there is no deadline.", + "type": "string", + "minLength": 1 + }, + "scheduled": { + "description": "Day the work is planned for (⏳), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "start": { + "description": "Earliest day work can begin (🛫), YYYY-MM-DD, calendar-validated.", + "type": "string", + "minLength": 1 + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier other tasks can name in depends_on.", + "type": "string", + "minLength": 1 + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs (🆔 values of other tasks). Non-empty; omit when there are no dependencies.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "subtasks": { + "description": "Checklist item descriptions — created as indented [ ] lines under the card (no metadata). For full sub-tasks with dates, priority, and block_id, make a separate call with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "format": { + "description": "Field format. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path", + "description", + "block_id" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_delete_note", + "title": "Delete Note", + "description": "Permanently delete a markdown note — removed from disk directly (no trash, no undo). After deletion, links to it from other notes become broken (detectable via vault_get_backlinks). Protected paths (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) are refused.\n\nExample: vault_delete_note({ path: \"Scratch/temp.md\" })\nExample: vault_delete_note({ path: \"Archive/2024/old.md\", prune_empty_folders: true }) — also remove \"Archive/2024\" (and \"Archive\") if deleting the note empties them.\n\nWhen to use: Removing a note you no longer need.\n\nBehavior: With prune_empty_folders, pruning is best-effort and runs after the delete — it never fails the call, so the note is always removed even if a folder can't be removed.\n\nErrors:\n- \"cannot delete protected path\" — the path sits under a protected folder\n- \"path traversal blocked\" — path escapes the vault root; use a vault-relative path\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not deletable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n- \"note not found: …\" — the note does not exist; verify the path with vault_list_notes before deleting\n\nReturns: Confirmation message, noting how many empty folders were pruned when any were.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path of the note to delete, including the \".md\" extension" + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the note's parent folder(s) if deleting it leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — a folder still holding any file, including a hidden one like .DS_Store, is left alone.", + "type": "boolean" + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place edits (this tool only deletes). To replace a block, prefer vault_replace_span (one atomic step); otherwise delete it here, then vault_patch_note to add the new content.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and entire lines are removed (never cuts mid-line). Omit end_anchor for a single-line delete.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is deleted.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the deletion are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\n\nReturns: Confirmation with lines removed and a truncated preview of the deleted text.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to delete just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "first_match": { + "description": "If an anchor matches more than one line, delete using the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. 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 is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_insert_at_anchor", + "title": "Insert at Anchor", + "description": "Insert content as whole lines before or after a specific line identified by a short anchor substring. Case-sensitive matching; an anchor matching more than one line is an error unless first_match is set. Same anchor resolution as vault_delete_span. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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- anchor locates a full line — the content is inserted as whole lines before or after it (never splits a line).\n- position: \"before\" inserts above the anchor line; \"after\" inserts below it.\n- content is inserted verbatim — blank lines inside it are kept, and a trailing newline adds a blank line after the inserted block.\n- first_match: when the anchor matches multiple lines, takes the first instead of erroring.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Inserted lines anchor in \" — N counts the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Notes/Plan.md\", \"Tracker.md\")" + }, + "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." + }, + "position": { + "type": "string", + "enum": [ + "before", + "after" + ], + "description": "\"before\" places the content on the lines above the anchor line; \"after\" places it on the lines below. The anchor line itself is never changed." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Content to insert (one or more lines), inserted verbatim as whole lines — blank lines are kept, and a trailing newline adds a blank line after the block." + }, + "first_match": { + "description": "If the anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "anchor", + "position", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Without this tool a move silently breaks every backlink.\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: \"Projects/Spec.md\" }) — move to another folder, updating links and the note's own relative links.\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 (About Me/ and the daily notes folder (read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json, defaulting to Daily Notes/)) cannot be moved.\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 it with vault_list_notes.\n- \"cannot move protected path …\" / \"cannot move into protected path …\" — old_path or new_path sits under a protected folder.\n- \"path must end in …\" — old_path or new_path is missing the .md extension; both paths must end in .md.\n- \"path traversal blocked\" — a path escapes the vault root; use vault-relative paths.\n- \"hidden path blocked\" — old_path or new_path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; notes cannot be moved from or into hidden paths, matching Obsidian.\n- \"concurrent write in progress\" — a write is in flight on the note, the destination, or one of its backlink sources (the move locks all of them as one unit); 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- Mid-move I/O failure (rare, e.g. a permission or disk error while writing) — the move aborts and the original note is deleted only after the destination and all backlinks are written, so a failure never loses data. The error message names what failed and the resulting state: if a backlink write failed, new_path exists and the original is intact (re-run the move, deleting the partial new_path first, to finish); if the final delete failed, both old_path and new_path exist (delete old_path to finish).\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. Only the target path is changed.\n\nReturns: JSON with moved_to (the new path), links_updated (count of link occurrences rewritten), updated_notes (sorted paths of the other notes that were edited; the moved note is implied by moved_to), and pruned_empty_folders (count of source folders removed — 0 unless prune_empty_folders was set).", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "minLength": 1, + "description": "Current vault-relative path of the note to move (e.g. \"Inbox/Draft.md\"). Must end in .md." + }, + "new_path": { + "type": "string", + "minLength": 1, + "description": "Destination vault-relative path (e.g. \"Projects/Spec.md\"). Must end in .md and must not already exist; parent folders are created as needed." + }, + "prune_empty_folders": { + "default": false, + "description": "When true, remove the source folder(s) if the move leaves them empty, walking up to (but never including) the vault root. Default false matches Obsidian, which leaves empty folders in place. Only removes a folder with zero entries — an in-place rename or a move into a subfolder of the source leaves it non-empty and prunes nothing.", + "type": "boolean" + } + }, + "required": [ + "old_path", + "new_path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_patch_note", + "title": "Patch Note", + "description": "Surgical edits to a markdown note — append, prepend, replace, or insert content by heading. Frontmatter values are preserved; YAML formatting may be normalized to block style on first edit.\n\nExample: vault_patch_note({ path: \"TASKS.md\", operation: \"append\", heading: \"Active\", content: \"- [ ] New task\" })\n\nCross-section move (e.g. completing a task on a board):\n1. vault_read_note to get current content and verify exact text\n2. vault_patch_note({ path, operation: \"append\", heading: \"Done\", content: \"- [x] Task text\" }) to add at target\n3. vault_replace_in_note({ path, old_text: \"- [ ] Task text\", new_text: \"\" }) to remove from source (for a large multi-line block, prefer vault_delete_span); on error, re-read and retry until the source copy is gone\nAdd at the target before deleting from the source — the two writes are not atomic, so this order can briefly duplicate the moved block on a failure but never lose it.\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.\n\nOperations:\n- append: add content at end of section (or end of file if no heading)\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 (heading preserved; requires heading; errors if the target has child headings unless include_children is set)\n- insert_before: insert content above the heading line (requires heading)\n\nHeading-targeted ops keep the matched heading and write content verbatim — don't begin content with the target heading (it's rejected to avoid a duplicate). No separator is added around the content — end it with a newline to leave a blank line after the inserted block.\n\nLimitation: A no-heading prepend inserts at body line 0. If the note has content above its first heading and your content starts with a heading, that content becomes the new section's body. The write still succeeds and the confirmation says so — use insert_before on the first heading to place a section above it instead.\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 in the parent section. 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- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or rename a heading if they share the same level\n- \"operation … requires a heading target\" — replace and insert_before need a heading\n- \"content begins with the heading … which would duplicate it\" — content's first line repeats the target heading; omit it (the matched heading is kept automatically)\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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\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 the call that would have avoided it.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"TASKS.md\", \"Projects/plan.md\")" + }, + "operation": { + "type": "string", + "enum": [ + "append", + "prepend", + "replace", + "insert_before" + ], + "description": "append | prepend | replace | insert_before. replace and insert_before require a heading; append and prepend work with or without one." + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Markdown content to insert, written verbatim with no separator added — end it with a newline to leave a blank line after the inserted block. Must not begin with the target heading text (it would duplicate the heading, which is kept automatically)." + }, + "heading": { + "description": "Target heading text (case-sensitive exact match). Required for replace and insert_before. Optional for append/prepend (omit for file-level operation).", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "include_children": { + "description": "When true, allows replace to overwrite a section that contains child headings. Without this, replace errors if children exist — preventing silent data loss.", + "type": "boolean" + } + }, + "required": [ + "path", + "operation", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_replace_in_note", + "title": "Replace in Note", + "description": "Find and replace text in a markdown note's body. Matches exact text (case-sensitive). Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only — properties must be edited via vault_update_properties or vault_write_note's properties parameter.\n\nExample: vault_replace_in_note({ path: \"Projects/plan.md\", old_text: \"TODO: write summary\", new_text: \"Summary complete.\" })\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, 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 is matched in the body only — frontmatter properties are never searched. Include enough surrounding context to ensure uniqueness when the target text appears in multiple places.\n- old_text + new_text together determine the operation: a non-empty new_text is an edit; an empty new_text (\"\") is a deletion. No regex — exact text only.\n- replace_all_occurrences (default false) replaces only the first match — a safety default when old_text appears in multiple places. 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- \"text not found\" — old_text does not appear in the note body; verify exact text with vault_read_note\n- \"oldText cannot be empty\" — old_text must be at least one character\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: new_text is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block in replacement text.\n\nReturns: Confirmation message with replacement count (number of occurrences replaced).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "old_text": { + "type": "string", + "minLength": 1, + "description": "Exact text to find (case-sensitive). Matches in the body only — text inside frontmatter properties is not searched." + }, + "new_text": { + "type": "string", + "description": "Replacement text. Empty string (\"\") deletes the matched text." + }, + "replace_all_occurrences": { + "description": "Replace all occurrences (default: false — replaces first occurrence only)", + "type": "boolean" + } + }, + "required": [ + "path", + "old_text", + "new_text" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Each anchor locates a full line — the entire line is selected, not just the matching substring. Same anchor semantics as vault_delete_span. Case-sensitive matching. Properties are preserved; YAML formatting may be normalized to block style on first edit. Operates on the body only.\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. Pick a short, unique fragment of the first line for start_anchor and, for a multi-line block, the last line for end_anchor.\nPrefer vault_replace_in_note for small in-place text changes (typos, renaming). Prefer vault_delete_span when removing without replacement.\n\nParameters:\n- start_anchor + end_anchor define a line range, not a text range — each anchor locates a full line, and the entire line from start to end is replaced (never cuts mid-line). Omit end_anchor for a single-line replace.\n- end_anchor is searched at or after the start line, so the span can never run backward. If both match the same line, only that one line is replaced.\n- content replaces the entire matched span and must be non-empty. A trailing newline adds a blank line after the new block.\n- first_match applies to both anchors independently — when an anchor matches multiple lines, takes the first instead of erroring.\n- Blank-line runs left by the replacement are collapsed to a single blank line.\n\nErrors:\n- \"note not found\" — verify path with vault_list_notes\n- \"anchor not found\" — fragment not on any 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- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\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\nObsidian syntax: content is Obsidian Flavored Markdown (no escaping applied). Watch for: #word = tag, [[ = wikilink, %% = comment block.\n\nReturns: Confirmation message \"Replaced lines with lines in \" — N counts the lines the span covered, M the lines content supplied.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Tracker.md\", \"Notes/Plan.md\")" + }, + "start_anchor": { + "type": "string", + "minLength": 1, + "description": "Short, unique substring that identifies the first line of the block (case-sensitive). The entire line is selected, not just the substring. Pick a brief fragment — do not paste the whole block." + }, + "end_anchor": { + "description": "Short, unique substring that identifies the LAST line of the block, searched at or after the start_anchor line. The entire line is selected. Omit to replace just the single line containing start_anchor.", + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Replacement content (one or more lines) — replaces every line of the matched span. Must be non-empty; use vault_delete_span to delete without replacement." + }, + "first_match": { + "description": "If an anchor matches more than one line, use the first match instead of erroring (default: false — ambiguity is an error).", + "type": "boolean" + } + }, + "required": [ + "path", + "start_anchor", + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_properties", + "title": "Update Properties", + "description": "Update a note's frontmatter properties via shallow merge — new keys added, matching keys overwritten, null deletes a key, unmentioned keys preserved. Body is never modified.\n\nExample: vault_update_properties({ path: \"Projects/todo.md\", properties: { status: \"active\", draft: null } })\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 }) — 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 traversal blocked\" — path escapes vault root\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not editable, matching Obsidian\n- \"concurrent write in progress\" — another write to this note is in flight; re-read the note and retry\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension" + }, + "properties": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {}, + "description": "Properties to merge. New keys are added; existing keys are overwritten; a null value deletes that key; unmentioned keys are preserved." + } + }, + "required": [ + "path", + "properties" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_update_task", + "title": "Update Task", + "description": "Update a task's status, priority, description, dates, dependencies, block_id, checklist items, or heading placement in one call. Any combination of these can change together — 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\", heading: \"Done\" }) — move a task to a different heading (lands at the top of the lane by default)\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", heading: \"Done\", position: \"bottom\" }) — move to the bottom of the lane\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", description: \"Updated task name\", due: \"2026-10-01\" }) — change description and set due date\nExample: vault_update_task({ path: \"TASKS.md\", block_id: \"my-task\", due: null }) — clear a date field\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\", task_id: \"abc123\" }) — set a Tasks plugin 🆔 identifier\n\nWhen to use: Any change to an existing task — completing, starting, re-prioritizing, editing text, setting or clearing dates, adding checklist items, assigning block_ids, or moving between headings. 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- path (required): vault-relative path to the note (must end in \".md\").\n- Exactly one of block_id or line is required to identify the task.\n- At least one change is required. Every field passed is applied in the same single write:\n - status: \"todo\" | \"in_progress\" | \"done\" | \"cancelled\". Manages checkbox and done/cancelled dates. On a Kanban board, \"done\" moves the card to the done lane together with its checklist sub-items (their checkboxes are left as they are); a sub-task marked done stays under its parent.\n - priority: \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" sets the signifier; null removes it.\n - description: replaces the task text. Metadata fields and block_id are preserved.\n - due / scheduled / start / created: YYYY-MM-DD sets the date; null clears it.\n - task_id: string sets the Tasks plugin 🆔; null clears it.\n - depends_on: non-empty string array sets the Tasks plugin ⛔; null clears it.\n - add_subtasks: non-empty string array — appends one indented [ ] checklist item per entry under the task; existing checklist items are kept. For full sub-tasks with their own metadata, use vault_create_task with parent_block_id.\n - assign_block_id: adds or replaces the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.\n - heading: target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.\n - position: \"top\" or \"bottom\" — where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\" (first position in the lane). Ignored when no heading move occurs.\n - Clearing is always explicit null — omitting a field leaves it untouched.\n- format: \"emoji\" or \"dataview\" — overrides the auto-detected Tasks plugin format.\n\nErrors:\n- \"note not found\" — path does not exist\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\" — no task line in the note ends with ^block_id\n- \"no task at line N\" — line doesn't contain a task checkbox\n- \"at least one mutation\" — no change params provided\n- \"cannot move a sub-task to a heading\" — explicit heading on a task nested under another task (depth > 0 in vault_list_tasks)\n- \"heading \"X\" not found; available: ...\" — target heading doesn't exist; the error lists the note's headings\n- \"multiple done lanes detected\" — status \"done\" on a Kanban board with more than one **Complete**-marked lane; pass heading to pick the lane\n- \"no done lane detected\" — status \"done\" on a Kanban board with no **Complete** marker and no \"Done\" heading; pass heading explicitly\n- \"blockId ... already exists\" / \"blockId ... contains invalid characters\" — assign_block_id must be unique in the note and match [a-zA-Z0-9-]+\n- \"invalid date\" — a date param fails calendar validation\n- \"description cannot be empty\" / \"dependsOn cannot be empty\" / \"addSubtasks cannot be empty\" / \"addSubtasks cannot contain an empty item\" — whitespace-only text or an empty array (use null to clear depends_on)\n- \"description must be a single line\" / \"addSubtasks items must be a single line\" — a task is one file line; a line break in the text would split its metadata onto a line the parser never reads\n- \"taskId ... contains invalid characters\" / \"dependsOn entry ... contains invalid characters\" — task_id and every depends_on entry must match [a-zA-Z0-9_-]+ (the Tasks plugin's id grammar)\n- \"concurrent write in progress\" — another write to this note is in flight; retry\n\nReturns: JSON { path, line, description, block_id, heading, subtasks, changes } — line is the final 1-based position; description is the current text; block_id and heading reflect the task after the update (block_id is omitted when the task has none, heading when the task sits above the first heading); subtasks lists each checklist item added by add_subtasks as { line, description } (omitted when none were added) — checklist items carry no block_id, so line is the handle for a follow-up update; changes lists every field applied as \"field: before → after\", with \"(none)\" for an absent value (for subtasks the two sides are checklist-item counts).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note containing the task (must end in \".md\")" + }, + "block_id": { + "description": "Stable task identifier — the ^block-id at the end of the task line, without the ^. Preferred over line.", + "type": "string", + "minLength": 1 + }, + "line": { + "description": "1-based line number from vault_list_tasks. Fragile if the file changed since the query.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "status": { + "description": "Target status. \"done\" appends the ✅ date and, on a Kanban board, moves the card and its checklist sub-items to the done lane (sub-item checkboxes are left as they are). \"cancelled\" appends the ❌ date.", + "type": "string", + "enum": [ + "todo", + "in_progress", + "done", + "cancelled" + ] + }, + "priority": { + "description": "Priority signifier to set, or null to remove it.", + "anyOf": [ + { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest" + ] + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "New task description text. Replaces the existing description; metadata fields and block_id are preserved.", + "type": "string", + "minLength": 1 + }, + "due": { + "description": "Due date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "scheduled": { + "description": "Scheduled date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "start": { + "description": "Start date (YYYY-MM-DD) to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "created": { + "description": "Created date (YYYY-MM-DD) to set or clear. Typically auto-stamped; use for corrections.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "task_id": { + "description": "Tasks plugin 🆔 identifier to set, or null to clear.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "depends_on": { + "description": "Tasks plugin ⛔ dependency IDs to set (non-empty), or null to clear.", + "anyOf": [ + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + { + "type": "null" + } + ] + }, + "add_subtasks": { + "description": "Checklist items to append, one indented [ ] line each, under the task's existing items — never replaces them. Can be combined with any other change. For full sub-tasks with metadata, use vault_create_task with parent_block_id.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "assign_block_id": { + "description": "Add or replace the ^block-id on the task line. Letters, digits, and hyphens only; must be unique within the note.", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Target heading to move the task to. On Kanban boards this is a lane move; works on any note with headings. Not valid on sub-tasks.", + "type": "string", + "minLength": 1 + }, + "position": { + "description": "Where within the target heading the task lands after a heading move or auto-done-lane move. Defaults to \"top\". Ignored when no heading move occurs.", + "type": "string", + "enum": [ + "top", + "bottom" + ] + }, + "format": { + "description": "Field format for new metadata. Default: auto-detected from .obsidian/ config, falling back to emoji.", + "type": "string", + "enum": [ + "emoji", + "dataview" + ] + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 — this is a full write, not a partial edit. Properties are passed separately and merged with any existing properties when overwriting (new keys added, matching keys overwritten, keys set to null removed, unmentioned keys preserved).\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\nLimitation: Writes the entire body. Do not use for surgical edits to large files — existing content will be lost unless you include it in the body parameter.\n\nErrors:\n- \"note already exists\" — a note already lives at this path; set overwrite: true to replace it, or use vault_patch_note / vault_replace_in_note for partial edits\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not writable, matching Obsidian\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\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: Confirmation message.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path including the \".md\" extension (e.g. \"Projects/notes.md\"). Parent folders are created as needed." + }, + "body": { + "type": "string", + "description": "Markdown body content — do not include frontmatter fences (---); use the properties parameter instead." + }, + "properties": { + "description": "Optional properties to merge. New keys are added; existing keys with matching names are overwritten; a null value deletes that key; unmentioned keys are preserved from the existing file.", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": {} + }, + "overwrite": { + "description": "Allow overwriting an existing note (default: false — errors if file exists).", + "type": "boolean" + } + }, + "required": [ + "path", + "body" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..010b0569 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -0,0 +1,1017 @@ +{ + "env": { + "READONLY_MODE": "true", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from About Me/ files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every matching dated entry sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off — EMBEDDING_ENABLED=false), 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range. search_mode is always \"fts\" and reranked always false in keyword-only mode.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — use specific keywords (semantic matching is off; re-query with synonyms to cover vocabulary drift)" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..951fbe77 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -0,0 +1,935 @@ +{ + "env": { + "READONLY_MODE": "true", + "FILE_TOOLS_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_get_memory to retrieve user preferences and context from About Me/ files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every matching dated entry sorted oldest-first, so the evolution of a preference, opinion, or fact reads in order. Matching is stemmed keywords only (semantic matching is off — EMBEDDING_ENABLED=false), 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range. search_mode is always \"fts\" and reranked always false in keyword-only mode.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — use specific keywords (semantic matching is off; re-query with synonyms to cover vocabulary drift)" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..8743a644 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -0,0 +1,934 @@ +{ + "env": { + "READONLY_MODE": "true", + "FILE_TOOLS_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes. Use vault_get_memory to retrieve user preferences and context from About Me/ files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every relevant dated entry 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\") whose relevance cut would come back empty 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range — so raise max_results or narrow the query for the complete set. search_mode is \"hybrid\" when vector matching contributed, \"fts\" when the entries came from keyword matching alone — including the any-term fallback that rescues a would-be-empty result; reranked is true when the cross-encoder relevance cut was applied.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — natural language works best (semantic matching bridges phrasing drift across months); content words about the topic rank better than meta framing (\"testing philosophy\" over \"opinions on testing\")" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} 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 new file mode 100644 index 00000000..da2baac2 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -0,0 +1,932 @@ +{ + "env": { + "READONLY_MODE": "true", + "MEMORY_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..d9b8e7db --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -0,0 +1,850 @@ +{ + "env": { + "READONLY_MODE": "true", + "MEMORY_ENABLED": "false", + "FILE_TOOLS_ENABLED": "false", + "EMBEDDING_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Full-text search across all vault notes, ranked by relevance. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"fts\" — keyword-only ranking), and reranked (always false in keyword-only mode). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..c9dbbf22 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -0,0 +1,849 @@ +{ + "env": { + "READONLY_MODE": "true", + "MEMORY_ENABLED": "false", + "FILE_TOOLS_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF); !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links).\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..807c8ce3 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -0,0 +1,931 @@ +{ + "env": { + "READONLY_MODE": "true", + "MEMORY_ENABLED": "false" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"Projects/plan.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and what needs follow-up.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, and recent notes." + } + ] +} 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 new file mode 100644 index 00000000..4c231d73 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -0,0 +1,1016 @@ +{ + "env": { + "READONLY_MODE": "true" + }, + "instructions": "Read and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from About Me/ files.\n\nVault content is Obsidian Flavored Markdown. No tools that modify the vault are available.", + "tools": [ + { + "name": "vault_find_orphans", + "title": "Find Orphans", + "description": "Find notes with no incoming links from other notes — 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 (self-links are ignored).\n\nExample: vault_find_orphans({ exclude_folders: [\"Daily Notes\",\"Templates\",\"About Me\"] })\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 ([\"Daily Notes\",\"Templates\",\"About Me\"]), it does not add to them — include the defaults yourself to keep them. Matched by folder prefix, recursing into subfolders (\"Projects\" also excludes \"Projects/Archive\").\n- limit (default 50) caps results after sorting by most-recently-modified.\n\nErrors:\n- An empty array means no orphans were found (after exclusions), not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "exclude_folders": { + "description": "Folders to exclude — replaces the defaults ([\"Daily Notes\",\"Templates\",\"About Me\"]), not merged", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max results (default 50)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_backlinks", + "title": "Get Backlinks", + "description": "Find all notes and files that link to a given note or canvas — captures [[wikilinks]], [markdown](links), ![[embeds]], wikilinks inside frontmatter properties (e.g. related:), and canvas file-node references. 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 with no backlinks at all, use vault_find_orphans.\n\nParameters:\n- path: exact vault-relative path including .md or .canvas extension, case-sensitive.\n\nReturns: JSON with path (the queried note or canvas), backlinks (array of { path, title, bytes } sorted by title), and count. Backlink sources may be notes (.md) or canvas files (.canvas).\n\nErrors: Rejects paths that don't end in .md or .canvas. A non-indexed path returns an empty result (count 0), not an error — use vault_list_notes or vault_search to discover valid paths.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Defaults to today if no date is provided.\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. Handles path resolution automatically using the vault's Obsidian config — you don't need to know the folder name or filename format. Use vault_recent_notes to review recent vault activity around a date (not date-filtered — returns globally recent notes).\n\nParameters:\n- date is ISO YYYY-MM-DD (e.g. \"2026-05-13\"). Defaults to today in the server's local timezone. Past and future dates are both valid — the tool resolves the configured path for any date and reports exists: false if the note hasn't been created yet. The path is derived from the server's daily-notes settings, so callers never need to construct daily note paths manually.\n\nErrors:\n- \"invalid date\" — use YYYY-MM-DD format (e.g. \"2026-05-13\", not \"May 13\")\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, or the L-family localized formats); change the format in Obsidian or set DAILY_NOTES_FORMAT to a supported alternative\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, the note has not been created yet.", + "inputSchema": { + "type": "object", + "properties": { + "date": { + "description": "YYYY-MM-DD (e.g. \"2026-05-13\", \"2025-12-31\"). Defaults to today in the server's timezone. Invalid formats like \"May 13\" return an error.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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: single file content. With file+section: just that H2 section's entries. No args: all files concatenated (frontmatter stripped) — can be large. Returns empty string when no memory files exist yet.\n\nExample: vault_get_memory({ file: \"Principles\", section: \"Decision heuristics (newest first)\" })\n\nWhen to use: Reading user preferences, principles, opinions, or other persistent context stored in About Me/ files. Call vault_list_memory_files first to discover valid file and section names.\nPrefer vault_read_note for reading non-memory notes.\n\nErrors:\n- \"section requires a file\" — section was provided without file; pass both or just 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\" — a dot-prefixed name would be a hidden file; memory files are always visible notes\n- \"section not found: …\" — no H2 heading matches; the error lists the file's available sections\n\nReturns: Raw markdown text.", + "inputSchema": { + "type": "object", + "properties": { + "file": { + "description": "Memory file name without .md (e.g. \"Principles\", \"Opinions\")", + "type": "string", + "minLength": 1 + }, + "section": { + "description": "H2 section heading (e.g. \"Decision heuristics (newest first)\"). Matched case-insensitively, with or without the \"(newest first)\" suffix. Call vault_list_memory_files first to discover valid names.", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_get_outgoing_links", + "title": "Get Outgoing Links", + "description": "Find all notes and files a given note or canvas links to. For notes: outgoing [[wikilinks]] and [markdown](links). For canvas files: file-node references (vault-relative paths embedded in the canvas). Links inside code blocks are ignored; self-links are included.\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: exact vault-relative path including .md or .canvas extension, case-sensitive. Matched against the search index, so the note or canvas must be indexed (file watcher processes new/moved files within seconds).\n\nReturns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes, daily_note_forward_ref } sorted by target path), and count. Each link carries exists (boolean) and kind (\"note\"|\"file\"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. daily_note_forward_ref is true on broken links into the vault's daily notes folder — expected \"create on click\" navigation to a daily note that doesn't exist yet, not genuine breakage. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.\n\nErrors: Rejects paths that don't end in .md or .canvas. A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Exact vault-relative path including .md or .canvas extension (e.g. \"Projects/vault-cortex.md\"). Case-sensitive." + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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({}) — every non-markdown file in the 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_search, vault_list_notes, and vault_search_by_folder cover only markdown notes, so this is the discovery surface for everything else. For the files one specific note links to, prefer vault_get_outgoing_links.\n\nParameters:\n- folder: folder path filter (e.g. \"attachments\" or \"Projects/media\"), searched recursively; omit for the whole vault\n- extensions: restrict to these extensions — case-insensitive, with or without the leading dot (\".png\" and \"png\" both work)\n- limit: maximum entries returned (default 50). extension_counts and total always reflect the full filtered set, not just the returned page.\n\nErrors:\n- A visible folder containing no files — or one that doesn't exist — returns an empty listing, not an error.\n- A folder path escaping the vault (e.g. \"../elsewhere\") is rejected with a path-traversal error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON with files (array of { path, extension, bytes }, sorted by path), extension_counts (per-extension totals over the full filtered set), total (full filtered count), and truncated (true when total exceeds limit). bytes is the on-disk file size, not the delivery cost: reading an image via vault_read_file returns a copy shrunk to fit when needed, so a large listed image is still cheap to read. Text formats return verbatim, so their listed size is what a read delivers. Files of supported types are readable via vault_read_file; vault_search covers markdown notes.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path to search recursively (e.g. \"attachments\"). Omit to list the whole vault.", + "type": "string", + "minLength": 1 + }, + "extensions": { + "description": "Only include these extensions, case-insensitive, leading dot optional (e.g. [\".png\", \"jpg\"]).", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limit": { + "description": "Max entries returned (default 50).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_memory_files", + "title": "List Memory Files", + "description": "Discovery tool — lists About Me/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a \"Scope of this file\" block describing what belongs in it). Does NOT return actual entries.\n\nExample: vault_list_memory_files() returns file outlines with headings like \"Decision heuristics (newest first)\", entry counts, each file's entry policy, and its scope callout.\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\nErrors:\n- An empty or nonexistent memory folder returns an empty array, not an error.\n\nReturns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is \"append-only\" (the default — entries are never edited or deleted) or \"living\" (a current-state file whose expired entries may be pruned; declared via `entry-policy` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a \"Scope of this file\" block, or null.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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\" })\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 scopes the listing to a path prefix (\"Projects\" includes \"Projects/Archive\"). When combined with glob, the glob pattern is applied within the folder's scope.\n- glob supports * (any filename chars) and ** (any path depth). Applied to vault-relative paths.\n\nErrors:\n- A nonexistent folder or no glob matches returns an empty array, not an error.\n- \"hidden path blocked\" — the folder is hidden (dot-prefixed, like \".obsidian\"); hidden folders are not listable, matching Obsidian.\n\nReturns: JSON array of vault-relative path strings (e.g. [\"Projects/plan.md\", \"Notes/idea.md\"]).", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Folder path prefix (e.g. \"About Me\", \"Projects\"). Includes all subfolders.", + "type": "string" + }, + "glob": { + "description": "Glob pattern for path filtering (e.g. \"**/*session-log*.md\"). Supports * and ** wildcards. Combined with folder when both are set.", + "type": "string" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_keys", + "title": "List Property Keys", + "description": "Discover all property keys in the vault with note counts and sample values. Lets you understand the vault's metadata schema without reading individual notes.\n\nExample: vault_list_property_keys() returns [{ key: \"tags\", count: 342, sample_values: [\"session-log\", \"project\"] }, ...]\n\nWhen to use: Discovering what properties exist before searching by property. Good first step for vault orientation alongside vault_list_tags.\nPrefer vault_list_property_values when you need the full list of values for a specific key. Prefer vault_search_by_property to find notes matching a specific key-value pair.\n\nParameters:\n- folder is matched as a path prefix and recurses into subfolders (\"Projects\" also covers \"Projects/Archive\"); omit it to scan the entire vault.\n\nReturns: JSON array of { key, count, sample_values } sorted by count descending. sample_values shows the top 3 most common values per key for quick orientation.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_property_values", + "title": "List Property Values", + "description": "List distinct values for a specific property key with note counts. Useful for discovering the range of values a property takes before searching.\n\nExample: vault_list_property_values({ key: \"status\" }) returns [{ value: \"active\", count: 47 }, { value: \"done\", count: 211 }, ...]\n\nWhen to use: Enumerating possible values for a property key before calling vault_search_by_property. Handles both scalar properties (status: \"active\") and array properties (tags: [\"a\", \"b\"]) — array elements are unpacked and counted individually, so the sum of counts may exceed the note count. An unknown key or empty folder returns an empty array, not an error. Call vault_list_property_keys first to discover valid key names.\n\nParameters:\n- key is case-sensitive and must match exactly as returned by vault_list_property_keys. Values are always strings — numeric and boolean properties are stringified for counting.\n- folder + key interact: folder restricts counting to a subtree, so the same key can return different value distributions depending on folder scope.\n- limit (default 50) applies after sorting by count descending, so you always get the most-used values first. Increase for high-cardinality keys like \"title\" or \"created\".\n\nReturns: JSON array of { value, count } sorted by count descending.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name — use vault_list_property_keys to discover valid keys (e.g. \"status\", \"type\", \"tags\")." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max values to return (default 50). Increase for high-cardinality properties.", + "type": "number" + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_list_tags", + "title": "List Tags", + "description": "List all tags in the vault with note counts, ordered by count descending. Only frontmatter tags are counted (inline #tags in note bodies are not indexed). Each hierarchical tag (e.g. \"project/vault-cortex\") appears as one full entry, not split into segments. Count is unique notes, not occurrences. A vault with no tagged notes returns an empty array.\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\nReturns: JSON array of { tag, count } sorted by count descending. tag omits the \"#\" prefix; count is unique notes with this tag.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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, 🆔/⛔ dependencies) and Dataview inline fields ([due:: 2026-07-04], [priority:: high], ...). Every result carries its attribution — note path, folder, line number, and the nearest heading when the task sits under one (the lane on a Kanban board) — so no follow-up reads are needed to locate a task. Task lines inside fenced code blocks and %% %% comment blocks are not indexed.\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; position is the natural sort for boards (file path then line number, preserving card arrangement)\nExample: vault_list_tasks({ folder: \"Code Projects/vault-cortex\" }) — all open tasks across a project tree (TASKS.md + task-notes/ subdirectories); folder is a recursive prefix match\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: 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) to read one specific board lane verbatim. Prefer vault_search for full-text queries over note content.\n\nParameters:\n- status: a single value or an array of values, OR-combined (default \"not_done\"). Values: \"not_done\" (todo + in_progress, excludes done AND cancelled), \"todo\", \"in_progress\", \"done\", \"cancelled\", \"all\". Virtual values expand in arrays: [\"not_done\", \"done\"] matches todo + in_progress + done.\n- due / scheduled / start / done / created / cancelled: date filters, each { before, on, after } in YYYY-MM-DD — before/after are exclusive, on is exact. A date filter only matches tasks that HAVE that date.\n- priority: array of \"highest\" | \"high\" | \"medium\" | \"low\" | \"lowest\" | \"none\", OR-combined (\"none\" = tasks with no priority signifier).\n- folder: recursive note-path prefix. tag: bare inline-task-tag name; a parent tag matches children. heading: exact heading text or array of headings, case-sensitive, OR-combined. path: one note, must end in \".md\".\n- top_level_only: boolean (default false). When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items.\n- sort_by: \"due\" (default) | \"scheduled\" | \"start\" | \"created\" | \"done\" | \"priority\" | \"note_mtime\" | \"position\". \"position\" sorts by file path then line number — the natural order for Kanban boards.\n- limit: max results (default 50). The total field always reports the full match count.\n\nErrors:\n- A malformed or calendar-invalid date filter throws with remediation text (\"Use YYYY-MM-DD\")\n- path without the \".md\" extension is rejected\n- No matches returns { total: 0, tasks: [] }, not an error\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 (absent = no checklist items); done counts status \"done\" only (a cancelled child counts toward total, not done), and the counts ignore the query's filters — so a filtered or top_level_only read still shows each card's checklist progress.", + "inputSchema": { + "type": "object", + "properties": { + "status": { + "description": "Status filter, OR-combined (default \"not_done\" = todo + in_progress, excluding done and cancelled). Virtual values expand in arrays: \"not_done\" adds todo + in_progress, \"all\" includes every status.", + "anyOf": [ + { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "not_done", + "todo", + "in_progress", + "done", + "cancelled", + "all" + ] + } + } + ] + }, + "due": { + "description": "Due date (📅 / [due:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "scheduled": { + "description": "Scheduled date (⏳ / [scheduled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "start": { + "description": "Start date (🛫 / [start:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "done": { + "description": "Done date (✅ / [completion:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "created": { + "description": "Created date (➕ / [created:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "cancelled": { + "description": "Cancelled date (❌ / [cancelled:: ]) bounds", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "priority": { + "description": "Priority levels, OR-combined; \"none\" selects tasks with no priority signifier", + "type": "array", + "items": { + "type": "string", + "enum": [ + "highest", + "high", + "medium", + "low", + "lowest", + "none" + ] + } + }, + "folder": { + "description": "Restrict to a note-path prefix (e.g. \"Code Projects/vault-cortex\")", + "type": "string", + "minLength": 1 + }, + "tag": { + "description": "Inline task tag, bare name without \"#\"; parent tags match children", + "type": "string", + "minLength": 1 + }, + "heading": { + "description": "Exact heading text or array of headings, OR-combined, case-sensitive (e.g. \"Active\" or [\"Active\", \"Up Next\"])", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "path": { + "description": "Restrict to one note (vault-relative path ending \".md\")", + "type": "string", + "minLength": 1 + }, + "top_level_only": { + "description": "When true, only top-level tasks (depth 0) are returned — excludes indented sub-tasks and checklist items. Default false.", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 50); total always reports the full match count", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "sort_by": { + "description": "Sort key (default \"due\"). Date sorts cascade through related fields when the primary is absent; each fallback uses its own natural direction. \"position\" sorts by file path then line number — the natural order for Kanban boards.", + "type": "string", + "enum": [ + "due", + "scheduled", + "start", + "created", + "done", + "priority", + "note_mtime", + "position" + ] + }, + "sort_direction": { + "description": "Sort direction. Default per field: \"asc\" for due/scheduled/priority/position, \"desc\" for start/created/done/note_mtime. Within a date cascade, each fallback uses its own default; an explicit value overrides all fields uniformly.", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 every relevant dated entry 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\") whose relevance cut would come back empty 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: \"opinions on testing\", 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\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. entries ascend by date (oldest first). total counts all matched entries; truncated=true means max_results dropped the least-relevant matches — never a date range — so raise max_results or narrow the query for the complete set. search_mode is \"hybrid\" when vector matching contributed, \"fts\" when the entries came from keyword matching alone — including the any-term fallback that rescues a would-be-empty result; reranked is true when the cross-encoder relevance cut was applied.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Topic to recall — natural language works best (semantic matching bridges phrasing drift across months); content words about the topic rank better than meta framing (\"testing philosophy\" over \"opinions on testing\")" + }, + "file": { + "description": "Optional: restrict to one memory file, name without .md (e.g. \"Opinions\"). Omit for cross-file recall — the default and usual choice.", + "type": "string", + "minLength": 1 + }, + "max_results": { + "description": "Cap on returned entries (default 50). When more match, the least-relevant are dropped and truncated=true — never a date range.", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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 to fit response limits when needed\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\" }) — a readable outline of the canvas\nExample: vault_read_file({ path: \"Boards/Roadmap.canvas\", raw: true }) — the canvas's exact JSON source\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, preceded by a metadata line stating the window and total line count\nExample: vault_read_file({ path: \"papers/research.pdf\" }) — structured text with title, headings, and links\nExample: vault_read_file({ path: \"papers/research.pdf\", raw: true }) — each page rendered as an image block\n\nWhat each type returns:\n- Images (.png/.jpg/.jpeg/.gif/.webp): the image as a viewable image block — downscaled and recompressed server-side when it exceeds client response limits, 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 to fit the budget.\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. Set raw: true for the exact JSON source instead (geometry, ids, colors — full fidelity).\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. Richer than flat text extraction: headings, code, and hyperlinks that flat extraction loses are preserved. Set raw: true for page images instead — each page rendered and returned as an image block, showing layout, diagrams, tables, and formatting that text extraction cannot preserve. Image-only and scanned PDFs work in raw mode. Up to 5 pages are rendered.\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- Line paging: start_line and limit page any text result — text formats, canvas outlines and raw JSON, PDF-extracted text — as a 1-based line window, preceded by a metadata line (\"data.csv — lines 51–100 of 400 (continue with start_line: 101)\"). Paged windows come back with \\n line endings and no trailing newline; a read without paging inputs stays 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. For .md notes use vault_read_note — this tool rejects them. To check a large file's size before reading it whole, request start_line: 1 with limit: 1 — one line plus the total line count.\n\nErrors:\n- \"not a file\" — the path ends in .md; read notes with vault_read_note\n- \"file not found\" — nothing exists at that path; discover valid paths via vault_list_files\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not readable, matching Obsidian\n- \"file too large\" — the file exceeds the server's read cap (MAX_FILE_BYTES, default 50 MiB)\n- \"text output too large\" — a text file or PDF renders past the 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/limit on an image or on a PDF with raw: true; line paging applies to text results only\n- \"not valid UTF-8\" — the file's bytes aren't UTF-8 text; returning them would silently corrupt the content\n- \"PDF has no extractable text\" — the PDF exists but contains no text content (scanned or image-only); states the page count. Set raw: true to render pages as images instead\n- \"PDF page rendering failed\" — raw: true was set but no pages could be rendered; the PDF may be corrupt\n- \"image cannot be fitted\" — the image could not be compressed under the output budget (MAX_IMAGE_OUTPUT_BYTES)\n- \"raw source is not available for images\" — raw applies to text-representable files; an image's delivered form is its image block\n- unsupported types (audio, archives, …) return an error naming 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.\n\nSearch coverage: vault_search indexes markdown notes plus canvas, PDF, and supported text-format content; find other files by browsing (vault_list_files) or through a note's links (vault_get_outgoing_links).", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the file, including its extension (e.g. \"attachments/photo.png\", \"Boards/Roadmap.canvas\"). Must NOT end in \".md\" — notes are read with vault_read_note." + }, + "raw": { + "description": "Return an alternative representation of the file. For .canvas this is the JSON Canvas source (geometry, ids, colors); for .pdf this renders pages as images instead of extracting text — useful for scanned documents, diagrams, and layout-sensitive content. Text formats already return their source, so raw changes nothing there. Images have no text source — raw returns an error.", + "type": "boolean" + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages any text result — text formats, canvas outlines and raw JSON, PDF-extracted text. Not valid for images or for PDFs with raw: true.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line. The output byte cap still applies to the window — reduce limit if it overflows.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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.\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 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 — both far cheaper than pulling the whole file. 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. 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. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\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\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"hidden path blocked\" — the path targets a hidden (dot-prefixed) file or folder like \".obsidian/\"; hidden paths are not accessible, matching Obsidian\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (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)\").\n\nOutline shape: { leading_callout?, leading_content?, headings } — headings is [{ level, text, bytes }]; leading_callout ({ type, title, body }) is the note's top-of-file callout; leading_content is the rest of the body text above the first heading, with the callout's own lines excluded so the two never repeat the same text. Either key is omitted when the note has none. 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.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Vault-relative path to the note, including the \".md\" extension (e.g. \"About Me/Principles.md\")" + }, + "properties_only": { + "description": "If true, returns parsed properties as JSON instead of full note content", + "type": "boolean" + }, + "outline": { + "description": "If true, returns { 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.", + "type": "boolean" + }, + "heading": { + "description": "Return only this section (heading line + body, through the next same-or-higher heading). Case-sensitive exact match.", + "type": "string", + "minLength": 1 + }, + "heading_level": { + "description": "Heading level (1-6) for disambiguation when multiple headings share the same text; only applies with heading", + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "start_line": { + "description": "First line to return, 1-based (default 1). Pages the delivered rendition (full body or a heading section). Not valid for outline or properties_only (JSON modes).", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "description": "Maximum lines returned (default: all remaining). A paged read's metadata line states the window, the total line count, and the next start_line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "path" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "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. Prefer vault_search_by_folder for browsing a specific folder.\n\nParameters:\n- sort_by + limit interact: \"modified\" (default) uses filesystem mtime, so every note has a value and limit works predictably. \"created\" uses the frontmatter created property — notes without it sort last (not excluded), so a small limit may return only notes that have the property; increase limit or use \"modified\" for broader coverage.\n- \"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\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted descending by chosen timestamp. created is null when the property is missing; bytes is on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "sort_by": { + "description": "Sort order (default \"modified\")", + "type": "string", + "enum": [ + "created", + "modified" + ] + }, + "limit": { + "description": "Max results (default 20, no upper cap)", + "type": "number" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search", + "title": "Search Notes", + "description": "Hybrid search across all vault notes, ranked by combined keyword and semantic relevance using Reciprocal Rank Fusion (RRF) — combining FTS5 keyword matching with vector similarity. Results are refined by a cross-encoder reranker using position-aware score blending when available. Semantic matching finds notes even when exact keywords differ — \"career aspirations\" finds notes about \"goals\" and \"targets\". Falls back to keyword-only (FTS5 BM25) transparently while embeddings are being built. Combine a text query with structured filters to narrow results by metadata — the \"narrow by metadata, search by text\" pattern. Unquoted terms use implicit AND with porter stemming; 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: path prefix (e.g. \"Projects\")\n- tags: require all listed tags (AND)\n- type: exact match on frontmatter type (e.g. \"person\", \"session-log\")\n- related: require all listed related links (AND)\n- properties: arbitrary frontmatter key-value pairs, supports string/number/boolean (e.g. { status: \"active\" })\n- created: date bounds { before, on, after } in YYYY-MM-DD on the frontmatter created property — before/after are exclusive, on is exact (calendar-day match, server-local). Notes without a parseable created property never match\n- modified: date bounds { before, on, after } in YYYY-MM-DD on filesystem modified time (server-local day boundaries) — before/after match strictly earlier/later days, on matches within the day\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: \"how the server watches for file changes\" }) — semantic: finds notes about chokidar and file watchers even without those exact terms\n\nWhen to use: The primary discovery tool for content-based queries, optionally constrained by metadata. Semantic matching bridges vocabulary gaps — 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 is sanitized automatically — the tool never throws a query syntax error\n- A malformed or calendar-invalid created/modified date filter throws with remediation text (\"Use YYYY-MM-DD\")\n\nReturns: JSON with results array (path, title, snippet, score, tags, folder, type, kind, extension, created, modified, bytes), total count, search_mode (\"hybrid\" or \"fts\"), and reranked (boolean — true when cross-encoder reranking refined the ordering). search_mode indicates which ranking was used — \"hybrid\" when vector embeddings contributed, \"fts\" when only keyword matching was available. score reflects combined relevance (higher = more relevant). kind is \"note\" for markdown notes or \"file\" for non-markdown content (canvas, PDF, and text files — .txt, .csv, .json, .xml, .svg, .log, .yaml, .yml, .base); file results also carry extension (e.g. \".canvas\", \".pdf\", \".txt\"). created is omitted when null. bytes is the on-disk file size. With filters.include_leading_callout, each result also carries leading_callout ({ type, title, body }) when present.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Search query text — unquoted terms use implicit AND with stemming; wrap in double quotes for exact phrases" + }, + "filters": { + "description": "Optional structured filters — all conditions AND-combine with each other and with the text query", + "type": "object", + "properties": { + "folder": { + "description": "Restrict to a folder path prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "tags": { + "description": "Require all listed tags (AND — every tag must be present)", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "related": { + "description": "Require all listed related links", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "type": { + "description": "Match the frontmatter type field (exact match, e.g. \"person\", \"meeting\")", + "type": "string", + "minLength": 1 + }, + "properties": { + "description": "Match arbitrary frontmatter properties by key-value (e.g. { status: \"active\", priority: 1 })", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "created": { + "description": "Created date bounds (YYYY-MM-DD) on the frontmatter \"created\" property — notes without a parseable value never match", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "modified": { + "description": "Modified date bounds (YYYY-MM-DD) on filesystem modified time, server-local day boundaries", + "type": "object", + "properties": { + "before": { + "description": "Exclusive upper bound (YYYY-MM-DD) — strictly earlier dates", + "type": "string", + "minLength": 1 + }, + "on": { + "description": "Exact date match (YYYY-MM-DD)", + "type": "string", + "minLength": 1 + }, + "after": { + "description": "Exclusive lower bound (YYYY-MM-DD) — strictly later dates", + "type": "string", + "minLength": 1 + } + } + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + }, + "snippet_tokens": { + "description": "Snippet length in tokens (default 30)", + "type": "number" + }, + "include_leading_callout": { + "description": "If true, each result includes its leading_callout ({ type, title, body }) when present. Off by default to keep results lean.", + "type": "boolean" + } + } + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_folder", + "title": "Search by Folder", + "description": "Browse notes in a folder with full metadata (tags, type, related, created, modified) — unlike vault_list_notes, which returns paths only.\n\nExample: vault_search_by_folder({ folder: \"Projects\" }) or vault_search_by_folder({ folder: \"About Me\", recursive: false })\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 is matched as a path prefix; pass it without a trailing slash (\"Projects\").\n- recursive (default true) includes all nested subfolders; set false to list only the folder's top level.\n- limit (default 20) caps results.\n\nErrors:\n- An empty or nonexistent folder returns an empty array, not an error.\n\nReturns: JSON array of note metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size.", + "inputSchema": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "minLength": 1, + "description": "Folder path (e.g. \"Projects\", \"About Me\")" + }, + "recursive": { + "description": "Include subfolders (default: true)", + "type": "boolean" + }, + "limit": { + "description": "Max results (default 20)", + "type": "number" + } + }, + "required": [ + "folder" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_property", + "title": "Search by Property", + "description": "Find notes where a frontmatter property matches a value — metadata-only search, no text query needed. Handles both scalar properties (status: \"active\") and array properties (tags, related): for arrays, matches if any element equals the value (contains check, not exact array match). Matching is exact and case-sensitive; an unknown key or unmatched value returns an empty array, not an error.\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 + value are both exact and case-sensitive — no partial matching or globbing. All property values are compared as strings, so numeric or boolean properties must be passed as their string representation.\n- For array properties (tags, related), value is tested against each element individually (contains check) — \"blog\" matches a note with tags: [\"blog\", \"draft\"] but not tags: [\"my-blog\"].\n- folder narrows results to a subtree; omit for vault-wide search. Combined with key+value, this lets you check how a property is used within a specific area.\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.", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Property key name (e.g. \"status\", \"type\", \"tags\"). Use vault_list_property_keys to discover valid keys." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Value to match (exact, case-sensitive, e.g. \"active\", \"session-log\"). Use vault_list_property_values to discover valid values for a key." + }, + "folder": { + "description": "Restrict to a folder prefix (e.g. \"Projects\")", + "type": "string", + "minLength": 1 + }, + "limit": { + "description": "Max results (default 20). Increase for broad metadata queries.", + "type": "number" + } + }, + "required": [ + "key", + "value" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "vault_search_by_tag", + "title": "Search by Tag", + "description": "Find notes with a specific tag. By default uses hierarchical prefix matching — a parent tag matches all children (e.g. \"project\" matches \"project/vault-cortex\", \"project/blog\"). Set exact=true for exact match only.\n\nExample: vault_search_by_tag({ tag: \"project\" }) returns all notes tagged project or project/*.\n\nWhen to use: Exploring tag hierarchies or finding all notes with a specific tag, without needing a text query.\nPrefer vault_search when you also need text-based relevance ranking. Use vault_list_tags first to discover available tags.\n\nParameters:\n- tag is the bare tag name without a leading \"#\" (\"project\", not \"#project\"). Hierarchical tags use \"/\" separators (\"project/vault-cortex\").\n- tag + exact interact: with exact=false (default), \"project\" matches \"project\", \"project/vault-cortex\", \"project/blog\" — the match is prefix-based on the \"/\" separator, so \"project\" does NOT match \"my-project\" or \"projects\". Set exact=true to match only the literal tag, excluding children.\n\nErrors:\n- An unknown tag or no matches returns an empty array, not an error — don't use as an existence check.\n\nReturns: JSON array of up to 20 notes' metadata (path, title, tags, related, folder, type, created, modified, bytes, leading_callout?, additional_properties), sorted by most recently modified. bytes is the on-disk file size. Promoted keys are in top-level fields; additional_properties contains only unpromoted keys.", + "inputSchema": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "minLength": 1, + "description": "Tag name without \"#\" prefix (e.g. \"project\", \"session-log\"). Hierarchical tags use \"/\" separators (e.g. \"project/vault-cortex\")." + }, + "exact": { + "description": "Exact match only (default: false, prefix match)", + "type": "boolean" + } + }, + "required": [ + "tag" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } + ], + "prompts": [ + { + "name": "daily-review", + "title": "Daily review & reconciliation", + "description": "Reconcile a day — daily note, vault-wide task status (due/overdue, scheduled), modified notes, and links — surface what happened, what's open, and durable facts worth saving to About Me/ memory.", + "arguments": [ + { + "name": "date", + "description": "Day to review in YYYY-MM-DD format (defaults to today)", + "required": false + }, + { + "name": "max_chars", + "description": "Optional cap on embedded content length (characters); omit for full content", + "required": false + } + ] + }, + { + "name": "vault-orientation", + "title": "Orient to this vault", + "description": "Survey this vault's structure and health — stats, folders, tags, properties (with adoption rates), orphans, recent notes, and the About Me/ memory layer." + } + ] +} diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts new file mode 100644 index 00000000..b932ccd8 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts @@ -0,0 +1,178 @@ +/** Captures the MCP wire surface — tool schemas, descriptions, annotations, + * prompts, and server instructions — per config combo, over a real in-process + * server. Feeds the committed baseline in __snapshots__/tool-surface/. */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import type { Prompt, Tool } from "@modelcontextprotocol/sdk/types.js" +import { loadConfig } from "../../config.js" +import { createSearchIndex } from "../../search/search-index.js" +import { computeEnabledToolNames, registerTools } from "../tool-definitions.js" +import { registerPrompts } from "../prompt-definitions.js" +import { buildServerMetadata } from "../mcp-router.js" +import type { Logger } from "../../../logger.js" + +type SurfaceAxis = { + envVar: string + /** The non-default value that changes the surface. */ + flippedValue: string + /** Short label used in the combo's snapshot filename. */ + label: string +} + +/** Boolean config axes that change the registered tool surface or its rendered + * text. The snapshot combos are the cross-product of this array — a new + * gating axis in config.ts must be added here, or its states go unpinned. */ +const SURFACE_AXES: readonly SurfaceAxis[] = [ + { envVar: "READONLY_MODE", flippedValue: "true", label: "readonly" }, + { envVar: "MEMORY_ENABLED", flippedValue: "false", label: "memory-off" }, + { + envVar: "FILE_TOOLS_ENABLED", + flippedValue: "false", + label: "file-tools-off", + }, + { + envVar: "EMBEDDING_ENABLED", + flippedValue: "false", + label: "embedding-off", + }, +] + +export type SurfaceCombo = { + /** Snapshot filename stem; multi-flip combos join axis labels with "+". */ + name: string + env: Readonly> +} + +/** All subsets of the axis list, built by extending every existing subset + * with and without each axis — 2^n subsets, the empty (default) one first. */ +const axisSubsets = SURFACE_AXES.reduce( + (subsets, axis) => { + const subsetsWithAxis = subsets.map((subset) => [...subset, axis]) + return [...subsets, ...subsetsWithAxis] + }, + [[]], +) + +const comboFromFlippedAxes = ( + flippedAxes: readonly SurfaceAxis[], +): SurfaceCombo => { + if (flippedAxes.length === 0) { + return { name: "default", env: {} } + } + return { + name: flippedAxes.map((axis) => axis.label).join("+"), + env: Object.fromEntries( + flippedAxes.map((axis) => [axis.envVar, axis.flippedValue]), + ), + } +} + +/** The 16 axis combos plus one DISABLED_TOOLS representative. vault_patch_note + * is cross-referenced from several other tools' descriptions, so that combo + * pins that references disappear with their target. No code branches on a + * conjunction of axes today; the conjunction combos pin rendered states that + * exist only there (several description clauses drop together) and + * future-proof later conjunction branching, at near-zero capture cost. */ +export const SURFACE_COMBOS: readonly SurfaceCombo[] = [ + ...axisSubsets.map(comboFromFlippedAxes), + { name: "disabled-tools", env: { DISABLED_TOOLS: "vault_patch_note" } }, +] + +const noop = (): void => {} +/** The drift test's assertion output is the report, so registration's + * per-group summary lines stay quiet. */ +const silentLogger: Logger = { + debug: noop, + info: noop, + warn: noop, + error: noop, + child: () => silentLogger, +} + +/** Fails loudly if the SDK ever starts paginating these lists — a truncated + * capture would silently understate the surface in the baseline. */ +const assertSinglePage = (listName: string, nextCursor?: string): void => { + if (nextCursor) { + throw new Error( + `${listName} returned a paginated response; the surface capture reads one page only`, + ) + } +} + +/** Bytewise name sort so registration-order refactors don't churn the + * baseline — list order is not part of the stability contract. */ +const sortByName = (items: readonly T[]): T[] => + items.toSorted((first, second) => (first.name < second.name ? -1 : 1)) + +export type SurfaceCapture = { + env: Readonly> + instructions: string + tools: readonly Tool[] + prompts: readonly Prompt[] +} + +/** + * Boots the real registration path for one combo and reads the surface a + * connected client sees — the SDK's own schema serialization, not a re-derived + * copy. The search index is an empty in-memory database and the vault path is + * never read: registration only declares metadata, and no tool handler runs. + */ +export const captureToolSurface = async ( + combo: SurfaceCombo, +): Promise => { + const config = loadConfig(combo.env) + const { instructions } = buildServerMetadata( + config, + computeEnabledToolNames(config), + ) + const server = new McpServer( + { name: "vault-cortex", version: "0.0.0" }, + { instructions }, + ) + const registrationContext = { + server, + vaultPath: "/vault", + search: createSearchIndex(":memory:", undefined, undefined, { + memoryDir: config.memoryDir, + }), + logger: silentLogger, + config, + } + registerTools(registrationContext) + registerPrompts(registrationContext) + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair() + const client = new Client({ name: "tool-surface-capture", version: "0.0.0" }) + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]) + + const toolsResult = await client.listTools() + const promptsResult = await client.listPrompts() + const capturedInstructions = client.getInstructions() + await client.close() + + assertSinglePage("tools/list", toolsResult.nextCursor) + assertSinglePage("prompts/list", promptsResult.nextCursor) + if (!capturedInstructions) { + throw new Error( + "server sent no instructions; buildServerMetadata always provides them", + ) + } + + return { + env: combo.env, + instructions: capturedInstructions, + tools: sortByName(toolsResult.tools), + prompts: sortByName(promptsResult.prompts), + } +} + +/** Byte-exact committed form: pre-serialized so vitest writes the file + * verbatim (the snapshot directory is prettier-ignored to keep it that way). */ +export const serializeSurfaceCapture = (capture: SurfaceCapture): string => + `${JSON.stringify(capture, null, 2)}\n` diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts new file mode 100644 index 00000000..6ac36631 --- /dev/null +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -0,0 +1,41 @@ +/** The committed wire-surface baseline: any byte drift in tool names, input + * schemas, descriptions, annotations, the prompt surface, or the server + * instructions fails here. Intentional changes regenerate the baseline via + * `npm run snapshot:update` and land as a reviewable diff in the same PR. + * The snapshot sees schemas and rendered text, not runtime response shapes — + * those stay enforced by the integration suite's exact assertions. */ + +import { readdirSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { describe, expect, it } from "vitest" +import { + SURFACE_COMBOS, + captureToolSurface, + serializeSurfaceCapture, +} from "./tool-surface-capture.js" + +describe("tool surface baseline", () => { + it.each(SURFACE_COMBOS.map((combo) => [combo.name, combo] as const))( + "combo %s matches the committed baseline", + async (comboName, combo) => { + const capture = await captureToolSurface(combo) + await expect(serializeSurfaceCapture(capture)).toMatchFileSnapshot( + `__snapshots__/tool-surface/${comboName}.json`, + ) + }, + ) + + // Guards two gaps vitest's file snapshots leave open: an orphaned file + // lingering after an axis change (nothing ever asserts it again), and a + // locally auto-created file that never went through a deliberate regen. + it("snapshot directory holds exactly one file per combo", () => { + const snapshotDirectory = fileURLToPath( + new URL("./__snapshots__/tool-surface/", import.meta.url), + ) + const committedFiles = readdirSync(snapshotDirectory).toSorted() + const expectedFiles = SURFACE_COMBOS.map( + (combo) => `${combo.name}.json`, + ).toSorted() + expect(committedFiles).toEqual(expectedFiles) + }) +}) diff --git a/src/vault-mcp/mcp-core/mcp-router.ts b/src/vault-mcp/mcp-core/mcp-router.ts index aa567727..bdec4c88 100644 --- a/src/vault-mcp/mcp-core/mcp-router.ts +++ b/src/vault-mcp/mcp-core/mcp-router.ts @@ -48,7 +48,7 @@ const SERVER_WEBSITE_URL = "https://github.com/aliasunder/vault-cortex" * enabled set so they track every gating axis; the capability framing * (read-only notice, memory-layer mention, search flavor) keys on config — * those describe the deployment, not a specific tool. */ -const buildServerMetadata = ( +export const buildServerMetadata = ( config: VaultConfig, enabledToolNames: ReadonlySet, ): { instructions: string; description: string } => { From c589a956b010457e5291404ecb27c7112392a9b6 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:29:45 -0400 Subject: [PATCH 2/3] docs: wire the tool-surface baseline into contributor checklists Co-Authored-By: Claude Fable 5 --- AGENTS.md | 58 +++++++++++++++++++++++++++++-------------------- CONTRIBUTING.md | 8 +++++++ package.json | 2 +- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b0c74f52..e1549d7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -798,7 +798,10 @@ continue }` over `if/else if` chains — each branch is 4. **Availability keying** — if the tool's description names other tools, use `whenToolEnabledText` so references disappear when their target is disabled. -5. **Feature-surface docs** — see the "Files that track feature +5. **Snapshot baseline** — run `npm run snapshot:update` and commit the + regenerated `__snapshots__/tool-surface/` files; any change to the + tool surface fails the drift test until the baseline matches. +6. **Feature-surface docs** — see the "Files that track feature surface" table below for which files to update (README tools table, ARCHITECTURE.md, DOCKERHUB regen, etc.). @@ -815,7 +818,10 @@ continue }` over `if/else if` chains — each branch is 4. **Availability keying** — use `whenToolEnabledText`, `isToolEnabled`, and `formatEnabledToolList` from the context for any tool references in the prompt text or fallback paths. -5. **Feature-surface docs** — update the README prompts table and +5. **Snapshot baseline** — run `npm run snapshot:update` and commit the + regenerated `__snapshots__/tool-surface/` files (prompts are part of + the captured surface). +6. **Feature-surface docs** — update the README prompts table and regenerate DOCKERHUB.md. ### MCP prompt conventions @@ -1033,7 +1039,10 @@ test. prefix (include test-controlled variable parts like paths and section names). - New config gating axis → config matrix test in - `server-integration.test.ts` (tool count + key behavior). + `server-integration.test.ts` (tool count + key behavior), and add the + axis to `SURFACE_AXES` in + `src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts` so the + snapshot combos cover it. - New prompt → assembly test verifying live vault data, not just the instruction wrapper. @@ -1279,27 +1288,28 @@ Several files outside `src/` reflect the project's feature surface and need updating alongside code changes. What to check depends on what changed: -| File | Update when… | -| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `README.md` | New deployment mode, new feature worth mentioning in the value prop | -| `ARCHITECTURE.md` | New component, requirement, or design decision; component diagram changes. Write for scannability: bullet lists and numbered pipelines over dense prose — a reader landing on this page should grasp the flow at a glance, not parse nested parentheticals. | -| `ROADMAP.md` | Direction genuinely changes — a priority shifts between Planned and Exploring, or a non-goal is decided. Refreshed in periodic passes (roughly once or twice a year) that remove completed Planned entries; not updated per feature PR, and shipped items are never promoted into Delivered — that section is a static foundations sketch, not a changelog | -| `server.json` | Description changes. `description` has a 100-character limit per the MCP registry schema — counted in code points, not bytes, so em dashes are safe here (CI guards it). | -| `Dockerfile` | OCI `image.description` label — keep in sync with `server.json` and `deploy.yml` descriptions | -| `assets/social-preview.svg` + `.png` | Feature category changes (rendered in the image); regenerate PNG after SVG edits (run `npm run render:social-preview`) | -| `.devin/wiki.json` | New architectural area (new page), module renamed/moved (update `repo_notes` or `purpose` references). Purposes stay structural — what the page covers and which modules — never capability narratives, counts, or tuning values; those live in README/ARCHITECTURE and DeepWiki derives them at index time. | -| `deploy/local/` + `deploy/remote/` | New env var, changed default, new deployment step, or Docker Compose service change — update `.env.example` and `README.md` in the affected directory | -| `render.yaml` + `deploy/render/` + `deploy/railway/` | A variable the image needs at boot is added or renamed, a shipped default changes (plan, disk size, hop count, health path, port), or the image tag changes. `templates.test.ts` pins `render.yaml`; the Railway template is re-published by hand from the definition table in `CONTRIBUTING.md` — existing deployments keep their settings until their owners redeploy | -| `.env.example` (root) | New env var or changed default for the Lightsail reference deployment | -| `cli/README.md` | Feature description or search capability changes — this is the npmjs.com landing page | -| `cli/src/env.ts` | Auto-synced optional blocks from `deploy/*/.env.example` via `npm run sync:cli-env-blocks` — run the script after editing deploy/ env files | -| `CONTRIBUTING.md` | CI pipeline, repo settings, or release conventions change | -| `DEPLOY.md` | Infrastructure, env vars, or deployment procedure changes | -| `GOVERNANCE.md` | Access to a sensitive resource changes — a registry, bot, or workflow credential is added or dropped, or a collaborator gains rights. The Access list and Continuity paragraph name concrete credentials, so they must track the workflows' secrets | -| `SECURITY.md` | The attack surface or its protections change — a new endpoint, auth layer, guard, scanner, or credential kind. The scope, hardening, setup-mode, and Secrets Management sections state what the code and CI actually do, and a stale claim here misleads vulnerability reporters | -| `DOCKERHUB.md` | Auto-generated — regenerate via `npm run generate:dockerhub-readme` when README.md changes tool/prompt tables, feature descriptions, env var table, or deployment options. Do not edit manually. | -| `.github/workflows/dockerhub-description.yml` | Description changes. Reads from `DOCKERHUB.md`. Docker Hub limits short descriptions to 100 UTF-8 **bytes**, not characters — an em dash costs 3 (CI guards the byte length). | -| `lhm.plugin.json` | Generated and gitignored — never edit or commit it. `npm run publish:lobehub` regenerates it from the live tool/prompt registry and publishes the LobeHub listing; that command is the only thing that needs running when tools, prompts, the `server.json` description, or the `package.json` keywords change (keywords become the listing's tags). | +| File | Update when… | +| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `README.md` | New deployment mode, new feature worth mentioning in the value prop | +| `ARCHITECTURE.md` | New component, requirement, or design decision; component diagram changes. Write for scannability: bullet lists and numbered pipelines over dense prose — a reader landing on this page should grasp the flow at a glance, not parse nested parentheticals. | +| `ROADMAP.md` | Direction genuinely changes — a priority shifts between Planned and Exploring, or a non-goal is decided. Refreshed in periodic passes (roughly once or twice a year) that remove completed Planned entries; not updated per feature PR, and shipped items are never promoted into Delivered — that section is a static foundations sketch, not a changelog | +| `server.json` | Description changes. `description` has a 100-character limit per the MCP registry schema — counted in code points, not bytes, so em dashes are safe here (CI guards it). | +| `Dockerfile` | OCI `image.description` label — keep in sync with `server.json` and `deploy.yml` descriptions | +| `assets/social-preview.svg` + `.png` | Feature category changes (rendered in the image); regenerate PNG after SVG edits (run `npm run render:social-preview`) | +| `.devin/wiki.json` | New architectural area (new page), module renamed/moved (update `repo_notes` or `purpose` references). Purposes stay structural — what the page covers and which modules — never capability narratives, counts, or tuning values; those live in README/ARCHITECTURE and DeepWiki derives them at index time. | +| `src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/` | Any change to tool schemas, descriptions, annotations, prompts, or server instructions — run `npm run snapshot:update` and commit the regenerated baseline in the same PR (the drift test fails until it matches). A dependency bump that changes the MCP SDK's schema serialization needs the same regen commit: that CI failure is the gate working, not flake. | +| `deploy/local/` + `deploy/remote/` | New env var, changed default, new deployment step, or Docker Compose service change — update `.env.example` and `README.md` in the affected directory | +| `render.yaml` + `deploy/render/` + `deploy/railway/` | A variable the image needs at boot is added or renamed, a shipped default changes (plan, disk size, hop count, health path, port), or the image tag changes. `templates.test.ts` pins `render.yaml`; the Railway template is re-published by hand from the definition table in `CONTRIBUTING.md` — existing deployments keep their settings until their owners redeploy | +| `.env.example` (root) | New env var or changed default for the Lightsail reference deployment | +| `cli/README.md` | Feature description or search capability changes — this is the npmjs.com landing page | +| `cli/src/env.ts` | Auto-synced optional blocks from `deploy/*/.env.example` via `npm run sync:cli-env-blocks` — run the script after editing deploy/ env files | +| `CONTRIBUTING.md` | CI pipeline, repo settings, or release conventions change | +| `DEPLOY.md` | Infrastructure, env vars, or deployment procedure changes | +| `GOVERNANCE.md` | Access to a sensitive resource changes — a registry, bot, or workflow credential is added or dropped, or a collaborator gains rights. The Access list and Continuity paragraph name concrete credentials, so they must track the workflows' secrets | +| `SECURITY.md` | The attack surface or its protections change — a new endpoint, auth layer, guard, scanner, or credential kind. The scope, hardening, setup-mode, and Secrets Management sections state what the code and CI actually do, and a stale claim here misleads vulnerability reporters | +| `DOCKERHUB.md` | Auto-generated — regenerate via `npm run generate:dockerhub-readme` when README.md changes tool/prompt tables, feature descriptions, env var table, or deployment options. Do not edit manually. | +| `.github/workflows/dockerhub-description.yml` | Description changes. Reads from `DOCKERHUB.md`. Docker Hub limits short descriptions to 100 UTF-8 **bytes**, not characters — an em dash costs 3 (CI guards the byte length). | +| `lhm.plugin.json` | Generated and gitignored — never edit or commit it. `npm run publish:lobehub` regenerates it from the live tool/prompt registry and publishes the LobeHub listing; that command is the only thing that needs running when tools, prompts, the `server.json` description, or the `package.json` keywords change (keywords become the listing's tags). | **Env var update checklist** — when adding, removing, or changing an env var that the server reads (defined in `config.ts`, `server.ts`, or diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4f6ecc9..cdf0104f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -187,6 +187,14 @@ merge even when the body is dropped. The `BREAKING CHANGE:` footer is preferred because it carries the descriptive line; the label and `!` only flag that a change is breaking. +The committed tool-surface baseline +(`src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/`) is the +byte-level record of the MCP wire surface. A PR that changes tool schemas, +descriptions, prompts, or server instructions regenerates it with +`npm run snapshot:update`, and the baseline diff is where reviewers judge +whether the change is breaking. The baseline captured at each release commit is +the stability contract's regression reference. + ## Release Process Releases are cut by the maintainer. Two paths: diff --git a/package.json b/package.json index 9d992c11..678e4184 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "markdownlint": "markdownlint-cli2 \"**/*.md\"", "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\"", "test": "vitest run", - "snapshot:update": "vitest run -u src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts", + "snapshot:update": "vitest run src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts --update", "test:coverage": "vitest run --coverage", "test:watch": "vitest", "test:cli-pty": "vitest run --config vitest.cli-pty.config.ts", From b25fa6bc8879588170477e863ae310c7762aa370 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:41:00 -0400 Subject: [PATCH 3/3] style: restructure SURFACE_COMBOS JSDoc as bullets, trim speculative clause Co-Authored-By: Claude Fable 5 --- .../mcp-core/__tests__/tool-surface-capture.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts index b932ccd8..eda02728 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-capture.ts @@ -69,12 +69,11 @@ const comboFromFlippedAxes = ( } } -/** The 16 axis combos plus one DISABLED_TOOLS representative. vault_patch_note - * is cross-referenced from several other tools' descriptions, so that combo - * pins that references disappear with their target. No code branches on a - * conjunction of axes today; the conjunction combos pin rendered states that - * exist only there (several description clauses drop together) and - * future-proof later conjunction branching, at near-zero capture cost. */ +/** The 16 axis combos plus one DISABLED_TOOLS representative: + * - vault_patch_note is cross-referenced from other tools' descriptions, so + * its combo verifies those references disappear when the tool is disabled. + * - Conjunction combos pin rendered states that exist only in multi-flip + * configs (several description clauses drop together). */ export const SURFACE_COMBOS: readonly SurfaceCombo[] = [ ...axisSubsets.map(comboFromFlippedAxes), { name: "disabled-tools", env: { DISABLED_TOOLS: "vault_patch_note" } },